From ac994484949aeb00deb35555e7da6c5e5332eb90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Mon, 3 Oct 2022 11:43:28 +0200 Subject: [PATCH 01/29] [NO-ISSUE] Refine the opengraph name of package packages --- src/FloraWeb/Server/Pages/Packages.hs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/FloraWeb/Server/Pages/Packages.hs b/src/FloraWeb/Server/Pages/Packages.hs index bd29ddc7..45aacbd0 100644 --- a/src/FloraWeb/Server/Pages/Packages.hs +++ b/src/FloraWeb/Server/Pages/Packages.hs @@ -80,7 +80,7 @@ showPackageVersion namespace packageName version = do let templateEnv = templateEnv' - { title = display namespace <> "/" <> display packageName <> " on Flora" + { title = display namespace <> "/" <> display packageName , description = release.metadata.synopsis } @@ -121,7 +121,7 @@ showDependentsHandler namespace packageName = do _ <- guardThatPackageExists namespace packageName let templateEnv = templateEnv' - { title = display namespace <> "/" <> display packageName <> " on Flora" + { title = display namespace <> "/" <> display packageName , description = "Dependents of " <> display namespace <> display packageName } results <- Query.getAllPackageDependentsWithLatestVersion namespace packageName @@ -138,7 +138,7 @@ showDependenciesHandler namespace packageName = do package <- guardThatPackageExists namespace packageName let templateEnv = templateEnv' - { title = display namespace <> "/" <> display packageName <> " on Flora" + { title = display namespace <> "/" <> display packageName , description = "Dependencies of " <> display namespace <> display packageName } releases <- Query.getAllReleases (package.packageId) @@ -167,7 +167,7 @@ listVersionsHandler namespace packageName = do package <- guardThatPackageExists namespace packageName let templateEnv = templateEnv' - { title = display namespace <> "/" <> display packageName <> " on Flora" + { title = display namespace <> "/" <> display packageName , description = "Releases of " <> display namespace <> display packageName } releases <- Query.getAllReleases (package.packageId) From d9754e249e3972e37839b7e1bfd001c099a9bc59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Mon, 3 Oct 2022 18:18:10 +0200 Subject: [PATCH 02/29] Create FUNDING.yml --- .github/FUNDING.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..0ac7d87c --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +# These are supported funding model platforms + +github: flora-pm +ko_fi: HecateMoonLight From afd75a6e208a5e2b859ab168d9da24ffeecf8d0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Tue, 4 Oct 2022 02:33:44 +0200 Subject: [PATCH 03/29] [FLORA-233] Reorder the columns of the package view in mobile view (#234) * [FLORA-223] Reorder the columns of the package view in mobile view The right column will be easier to access on phone, as it will not be hidden by the README body. * Underline version links in package view * Systematise the error page upon a non-existent route * Lint * changelog entry --- CHANGELOG.md | 4 ++++ assets/css/app.css | 40 ++++++++++++++++++++++++++++++--- src/FloraWeb/Server.hs | 26 ++++++++++++++++++--- src/FloraWeb/Templates/Error.hs | 4 ++-- 4 files changed, 66 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20291958..a304e56f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## v1.0.5 -- XXXX-XX-XX + +* Reorder the package page columns in mobile view (#233) + ## v1.0.4 -- 2022-10-02 * Colourise the search bars on focus (#215) diff --git a/assets/css/app.css b/assets/css/app.css index 2abbd713..bdb10c10 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -111,14 +111,21 @@ a { } .error-code { - --tw-bg-opacity: 1; + color: var(--error-code); + font-size: 3rem; + line-height: 1; + font-weight: 800; +} +.error-message { + letter-spacing: -0.025em; + font-weight: 800; + font-size: 3rem; + line-height: 1; color: var(--error-code); } .error-page-button { - --tw-bg-opacity: 1; - background-color: var(--error-button); } @@ -174,6 +181,9 @@ div[class="bullets"] { } .package-body { + display: flex; + flex-direction: column; + a { font-weight: 500; } @@ -268,6 +278,10 @@ div[class="bullets"] { text-decoration: underline; } +.release a:hover { + text-decoration: underline; +} + .package-list { .package-list-item { font-size: 1.25rem; @@ -408,4 +422,24 @@ div[class="bullets"] { .dependency { white-space: nowrap; } + + .package-body { + flex-direction: row; + } +} + +/* smaller display rules */ + +@media (max-width: px) { + .package-left-column { + order: 1; + } + + .package-right-column { + order: 2; + } + + .package-readme-column { + order: 3; + } } diff --git a/src/FloraWeb/Server.hs b/src/FloraWeb/Server.hs index 9c1429c9..51c3fde7 100644 --- a/src/FloraWeb/Server.hs +++ b/src/FloraWeb/Server.hs @@ -24,12 +24,18 @@ import Prometheus.Metric.Proc (procMetrics) import Servant ( Application , Context (..) + , ErrorFormatters , Handler , HasServer (hoistServerWithContext) + , NotFoundErrorFormatter , Proxy (Proxy) + , defaultErrorFormatters + , err404 , hoistServer + , notFoundErrorFormatter , serveDirectoryWebApp ) +import Servant.API (getResponse) import Servant.Server.Generic (AsServerT, genericServeTWithContext) import Control.Exception.Safe qualified as Safe @@ -47,6 +53,7 @@ import Data.Function ((&)) import Data.Pool (Pool) import Database.PostgreSQL.Simple (Connection) import Effectful.Dispatch.Static +import Effectful.Error.Static (runErrorNoCallStack) import Effectful.PostgreSQL.Transact.Effect (runDB) import Flora.Environment (DeploymentEnv, FloraEnv (..), LoggingEnv (..), getFloraEnv) import Flora.Environment.OddJobs qualified as OddJobs @@ -63,8 +70,10 @@ import FloraWeb.Server.Metrics import FloraWeb.Server.OpenSearch import FloraWeb.Server.Pages qualified as Pages import FloraWeb.Server.Tracing +import FloraWeb.Templates (defaultTemplateEnv, defaultsToEnv) +import FloraWeb.Templates.Error (renderError) import FloraWeb.Types -import Servant.API (getResponse) +import Network.HTTP.Types (notFound404) runFlora :: IO () runFlora = bracket (runEff getFloraEnv) (runEff . shutdownFlora) $ \env -> runEff . runCurrentTimeIO . runConcurrent $ do @@ -171,5 +180,16 @@ naturalTransform deploymentEnv logger webEnvStore app = & runLog deploymentEnv logger & effToHandler -genAuthServerContext :: Logger -> FloraEnv -> Context '[FloraAuthContext] -genAuthServerContext logger floraEnv = authHandler logger floraEnv :. EmptyContext +genAuthServerContext :: Logger -> FloraEnv -> Context '[FloraAuthContext, ErrorFormatters] +genAuthServerContext logger floraEnv = authHandler logger floraEnv :. errorFormatters :. EmptyContext + +errorFormatters :: ErrorFormatters +errorFormatters = + defaultErrorFormatters{notFoundErrorFormatter = notFoundPage} + +notFoundPage :: NotFoundErrorFormatter +notFoundPage _req = + let result = runPureEff $ runErrorNoCallStack $ renderError (defaultsToEnv defaultTemplateEnv) notFound404 + in case result of + Left err -> err + Right _ -> err404 diff --git a/src/FloraWeb/Templates/Error.hs b/src/FloraWeb/Templates/Error.hs index 366b8d18..109a9b92 100644 --- a/src/FloraWeb/Templates/Error.hs +++ b/src/FloraWeb/Templates/Error.hs @@ -36,10 +36,10 @@ showError status = do div_ [class_ "px-4 py-2 sm:px-6 sm:py-24 md:grid md:place-items-center lg:px-8"] $ div_ [class_ "max-w-max mx-auto"] $ main_ [class_ "sm:flex"] $ do - p_ [class_ "lg:text-5xl font-extrabold sm:text-5xl error-code"] $ toHtml $ show $ statusCode status + p_ [class_ "error-code"] $ toHtml $ show $ statusCode status div_ [class_ "sm:ml-6"] $ do div_ [class_ "sm:border-l sm:border-gray-200 sm:pl-6"] $ do - h1_ [class_ "text-6xl font-extrabold dark:text-gray-100 text-gray-900 tracking-tight sm:text-5xl"] $ + h1_ [class_ "text-5xl error-message"] $ toHtml $ statusMessage status div_ [class_ "mt-10 flex space-x-3 sm:border-l sm:border-transparent sm:pl-6"] $ do From fc22e2b003d688fc0fb9166e0a27357a01f720c9 Mon Sep 17 00:00:00 2001 From: Nathan van Doorn Date: Sat, 8 Oct 2022 12:09:52 +0200 Subject: [PATCH 04/29] [FLORA-205] Enable the use of markdown extensions (#236) * Enable the use of markdown extensions * Run fourmolu (secretly I pushed so I could use the CI's version, mwahahaha) * Actually read the output of fourmolu * I really don't like how fourmolu does this but I must trust the machine * Add note in changelog --- CHANGELOG.md | 1 + src/Flora/OddJobs.hs | 21 ++++++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a304e56f..4c0e7d94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## v1.0.5 -- XXXX-XX-XX * Reorder the package page columns in mobile view (#233) +* Enable the use of markdown extensions in package READMEs (#236) ## v1.0.4 -- 2022-10-02 diff --git a/src/Flora/OddJobs.hs b/src/Flora/OddJobs.hs index d664359f..86e8c438 100644 --- a/src/Flora/OddJobs.hs +++ b/src/Flora/OddJobs.hs @@ -19,6 +19,7 @@ module Flora.OddJobs where import Commonmark qualified +import Commonmark.Extensions qualified import Control.Concurrent (forkIO) import Control.Exception import Control.Monad @@ -122,9 +123,23 @@ makeReadme pay@MkReadmePayload{..} = localDomain "fetch-readme" $ do logInfo ("got a body for package " <> display mpPackage) (object ["release_id" .= mpReleaseId]) htmlTxt <- do - -- let extensions = emojiSpec - -- Commonmark.commonmarkWith extensions ("readme " <> show mpPackage) bodyText - pure (Commonmark.commonmark ("readme " <> show mpPackage) bodyText) + let extensions = + mconcat + [ Commonmark.Extensions.mathSpec + , -- all gfm extensions apart from pipeTable + Commonmark.Extensions.emojiSpec + , Commonmark.Extensions.strikethroughSpec + , Commonmark.Extensions.autolinkSpec + , Commonmark.Extensions.autoIdentifiersSpec + , Commonmark.Extensions.taskListSpec + , Commonmark.Extensions.footnoteSpec + , -- default syntax + Commonmark.defaultSyntaxSpec + , -- pipe table spec. This has to be after default syntax due to + -- https://github.com/jgm/commonmark-hs/issues/95 + Commonmark.Extensions.pipeTableSpec + ] + Commonmark.commonmarkWith extensions ("readme " <> show mpPackage) bodyText >>= \case Left exception -> throw (MarkdownFailed exception) Right (y :: Commonmark.Html ()) -> pure $ Commonmark.renderHtml y From 7bc1183114318602cb728289bf96ed302a0b1f22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Sat, 8 Oct 2022 14:21:05 +0200 Subject: [PATCH 05/29] [NO-ISSUE] Update vector deps --- flora.cabal | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flora.cabal b/flora.cabal index 52578486..a59023a9 100644 --- a/flora.cabal +++ b/flora.cabal @@ -206,7 +206,7 @@ library , lucid-aria ^>=0.1 , lucid-svg ^>=0.7 , monad-control ^>=1.0 - , monad-time ^>=0.3 + , monad-time ^>=0.4 , mtl ^>=2.2 , odd-jobs , optics-core ^>=0.4 @@ -248,8 +248,8 @@ library , typed-process ^>=0.2 , unordered-containers ^>=0.2 , uuid ^>=1.3 - , vector ^>=0.12 - , vector-algorithms ^>=0.8 + , vector ^>=0.13 + , vector-algorithms ^>=0.9 , wai ^>=3.2 , wai-log ^>=0.3 , wai-middleware-heartbeat ^>=0.0 From f0fc03b41667fe1735eb1644814516ea2bd9f6f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Sun, 9 Oct 2022 09:40:35 +0200 Subject: [PATCH 06/29] [NO-ISSUE] Add CONTRIBUTING instructions --- CONTRIBUTING.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 26c122af..6493acb8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,9 +14,7 @@ The following Haskell command-line tools will have to be installed: * `ghcid`: To automatically reload the Haskell code base upon source changes * `ghc-tags`: To generate ctags or etags for the project -```bash -$ cabal install -j postgresql-migration fourmolu hlint apply-refact cabal-fmt nixfmt ghcid ghc-tags -``` +(Some of those packages have incompatible dependencies, so don't try to install them all at once with cabal) * `yarn`: The tool that handles the JavaScript code bases * `esbuild`: The tool that handles asset bundling @@ -28,6 +26,12 @@ You need to * Read this document * Have a ticket that you can relate the PR to, so that we can have some context for your change * Provide screenshots of before/after if you change the UI. +* Put `[FLORA-XXXX]` where XXXX is the ticket this PR is related to, or [NO-ISSUE] if no tickets are related, in the +PR title and commit message: + +``` +[NO-ISSUE] Update dependencies for Storybook.js +``` ### Feature request From a7422661a936205e9e45b811156a38a740ad5128 Mon Sep 17 00:00:00 2001 From: Alexandre Tunstall <32900877+AlexandreTunstall@users.noreply.github.com> Date: Sun, 9 Oct 2022 08:48:09 +0100 Subject: [PATCH 07/29] [FLORA-235] Autofocus the search field on the home page (#238) --- CHANGELOG.md | 1 + src/FloraWeb/Templates/Pages/Home.hs | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c0e7d94..812ccc23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Reorder the package page columns in mobile view (#233) * Enable the use of markdown extensions in package READMEs (#236) +* Autofocus the search field on the home page (#235) ## v1.0.4 -- 2022-10-02 diff --git a/src/FloraWeb/Templates/Pages/Home.hs b/src/FloraWeb/Templates/Pages/Home.hs index 6eabf4b5..bd264a9f 100644 --- a/src/FloraWeb/Templates/Pages/Home.hs +++ b/src/FloraWeb/Templates/Pages/Home.hs @@ -95,6 +95,7 @@ searchBar = , placeholder_ "Find a package" , value_ "" , tabindex_ "1" + , autofocus_ ] button_ [type_ "submit", class_ "items-center right-0 top-0 mt-5 mr-4 mb-5"] $ svg_ [xmlns_ "http://www.w3.org/2000/svg", class_ "h-6 w-6 my-auto m-2", style_ "color: gray", fill_ "none", viewBox_ "0 0 24 24", stroke_ "currentColor"] $ From 7193ce2ca09c0f6987fd5e99e995ca2bfaee464a Mon Sep 17 00:00:00 2001 From: Gabriella Gonzalez Date: Sun, 9 Oct 2022 11:01:07 +0200 Subject: [PATCH 08/29] [NO-ISSUE] Refactor `FloraWeb.Templates.render` (#239) --- src/FloraWeb/Templates.hs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/FloraWeb/Templates.hs b/src/FloraWeb/Templates.hs index d568c044..16994cdd 100644 --- a/src/FloraWeb/Templates.hs +++ b/src/FloraWeb/Templates.hs @@ -18,9 +18,7 @@ import FloraWeb.Components.Header (header) import FloraWeb.Templates.Types as Types render :: (Monad m) => TemplateEnv -> FloraHTML -> m (Html ()) -render env template = - let deploymentEnv = env.environment - in pure $ toHtmlRaw $ runIdentity $ runReaderT (renderBST (rendered deploymentEnv template)) env +render env template = pure (renderUVerb env template) renderUVerb :: TemplateEnv -> FloraHTML -> Html () renderUVerb env template = From fae39c26592c6e9e1fea37f82e96089ae0e02aa5 Mon Sep 17 00:00:00 2001 From: Gabriella Gonzalez Date: Sun, 9 Oct 2022 13:37:05 +0200 Subject: [PATCH 09/29] Enable flake support for development (#240) --- default.nix | 7 ++ flake.lock | 43 ++++++++ flake.nix | 176 ++++++++++++++++++++++++++++++ flora.cabal | 14 +-- nix/envparse.nix | 16 +++ nix/gitignoreSource.nix | 8 -- nix/log-effectful.nix | 24 ++++ nix/odd-jobs.nix | 57 ++++++++++ nix/pg-transact-effectful.nix | 18 +++ nix/pin.nix | 4 - nix/pin2.nix | 4 - nix/prometheus-client.nix | 33 ++++++ nix/prometheus-metrics-ghc.nix | 21 ++++ nix/raven-haskell.nix | 25 +++++ nix/servant-effectful.nix | 22 ++++ nix/time-effectful.nix | 19 ++++ nix/wai-middleware-heartbeat.nix | 14 +++ nix/wai-middleware-prometheus.nix | 22 ++++ shell.nix | 47 ++------ 19 files changed, 511 insertions(+), 63 deletions(-) create mode 100644 default.nix create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 nix/envparse.nix delete mode 100644 nix/gitignoreSource.nix create mode 100644 nix/log-effectful.nix create mode 100644 nix/odd-jobs.nix create mode 100644 nix/pg-transact-effectful.nix delete mode 100644 nix/pin.nix delete mode 100644 nix/pin2.nix create mode 100644 nix/prometheus-client.nix create mode 100644 nix/prometheus-metrics-ghc.nix create mode 100644 nix/raven-haskell.nix create mode 100644 nix/servant-effectful.nix create mode 100644 nix/time-effectful.nix create mode 100644 nix/wai-middleware-heartbeat.nix create mode 100644 nix/wai-middleware-prometheus.nix diff --git a/default.nix b/default.nix new file mode 100644 index 00000000..8736ee96 --- /dev/null +++ b/default.nix @@ -0,0 +1,7 @@ +(import ( + fetchTarball { + url = "https://github.com/edolstra/flake-compat/archive/b4a34015c698c7793d592d66adbab377907a2be8.tar.gz"; + sha256 = "1qc703yg0babixi6wshn5wm2kgl5y1drcswgszh4xxzbrwkk9sv7"; } +) { + src = ./.; +}).defaultNix diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..1d206d66 --- /dev/null +++ b/flake.lock @@ -0,0 +1,43 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1665087388, + "narHash": "sha256-FZFPuW9NWHJteATOf79rZfwfRn5fE0wi9kRzvGfDHPA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "95fda953f6db2e9496d2682c4fc7b82f959878f7", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "utils": "utils" + } + }, + "utils": { + "locked": { + "lastModified": 1659877975, + "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..55b50716 --- /dev/null +++ b/flake.nix @@ -0,0 +1,176 @@ +{ inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + + utils.url = github:numtide/flake-utils; + }; + + outputs = { nixpkgs, utils, ... }: + utils.lib.eachSystem [ "x86_64-linux" "x86_64-darwin" ] (system: + let + compiler = "ghc92"; + + config = { allowBroken = true; allowUnsupportedSystem = true; }; + + overlay = pkgsNew: pkgsOld: { + flora = + pkgsNew.haskell.lib.justStaticExecutables + (pkgsNew.overrideCabal + pkgsNew.haskellPackages.flora + (old: { + nativeBuildInputs = (old.nativeBuildInputs or []) ++ [ + pkgsNew.makeWrapper + ]; + + postInstall = (old.postInstall or "") ++ + '' + wrapProgram $out/bin/flora-cli --prefix PATH : ${pkgsNew.souffle}/bin + ''; + }) + ); + + haskell = + pkgsOld.haskell // { + packages = pkgsOld.haskell.packages // { + "${compiler}" = + pkgsOld.haskell.packages."${compiler}".override (old: { + overrides = + pkgsNew.lib.fold + pkgsNew.lib.composeExtensions + (old.overrides or (_: _: { })) + [ (pkgsNew.haskell.lib.packageSourceOverrides { + flora = ./.; + + text-display = "0.0.2.0"; + }) + (pkgsNew.haskell.lib.packagesFromDirectory { + directory = ./nix; + }) + (haskellPackagesNew: haskellPackagesOld: { + Cabal-syntax = haskellPackagesNew.Cabal_3_8_1_0; + + lens-aeson = haskellPackagesNew.lens-aeson_1_2_2; + + log-effectful = + pkgsNew.haskell.lib.doJailbreak + haskellPackagesOld.log-effectful; + + monad-time = haskellPackagesNew.monad-time_0_4_0_0; + + odd-jobs = + pkgsNew.haskell.lib.overrideCabal + haskellPackagesOld.odd-jobs + (old: { + doCheck = false; + + prePatch = ""; + + libraryToolDepends = []; + }); + + PyF = haskellPackagesNew.PyF_0_11_1_0; + + pcre2 = + pkgsNew.haskell.lib.dontCheck + haskellPackagesOld.pcre2; + + pg-entity = + pkgsNew.haskell.lib.doJailbreak + (pkgsNew.haskell.lib.dontCheck + haskellPackagesOld.pg-entity + ); + + postgresql-simple-migration = + pkgsNew.haskell.lib.doJailbreak + haskellPackagesOld.postgresql-simple-migration; + + raven-haskell = + pkgsNew.haskell.lib.dontCheck + haskellPackagesOld.raven-haskell; + + resource-pool = + haskellPackagesNew.resource-pool_0_3_1_0; + + servant-static-th = + pkgsNew.haskell.lib.dontCheck + haskellPackagesOld.servant-static-th; + + slugify = + pkgsNew.haskell.lib.dontCheck + haskellPackagesOld.slugify; + + souffle-haskell = + pkgsNew.haskell.lib.dontCheck + haskellPackagesOld.souffle-haskell; + + pg-transact = + pkgsNew.haskell.lib.dontCheck + haskellPackagesOld.pg-transact; + + pg-transact-effectful = + pkgsNew.haskell.lib.doJailbreak + haskellPackagesOld.pg-transact-effectful; + + prometheus-proc = + pkgsNew.haskell.lib.doJailbreak + haskellPackagesOld.prometheus-proc; + + text-metrics = + pkgsNew.haskell.lib.doJailbreak + haskellPackagesOld.text-metrics; + + type-errors-pretty = + pkgsNew.haskell.lib.doJailbreak + (pkgsNew.haskell.lib.dontCheck + haskellPackagesOld.type-errors-pretty + ); + + wai-middleware-heartbeat = + pkgsNew.haskell.lib.doJailbreak + haskellPackagesOld.wai-middleware-heartbeat; + + vector = + pkgsNew.haskell.lib.overrideCabal + haskellPackagesNew.vector_0_13_0_0 + (old: { + testHaskellDepends = (old.testHaskellDepends or []) ++ [ + haskellPackagesNew.doctest + ]; + }); + + vector-algorithms = + haskellPackagesNew.vector-algorithms_0_9_0_1; + }) + ]; + }); + }; + }; + }; + + pkgs = + import nixpkgs { inherit config system; overlays = [ overlay ]; }; + + in + rec { + packages.default = pkgs.haskell.packages."${compiler}".flora; + + apps = rec { + default = server; + + server = { + type = "app"; + + program = "${pkgs.flora}/bin/flora-server"; + }; + + cli = { + type = "app"; + + program = "${pkgs.flora}/bin/flora-cli"; + }; + }; + + devShells.default = pkgs.haskell.packages."${compiler}".flora.env; + } + ); +} + diff --git a/flora.cabal b/flora.cabal index a59023a9..f4d81694 100644 --- a/flora.cabal +++ b/flora.cabal @@ -171,11 +171,11 @@ library Lucid.Orphans build-depends: - , aeson <=1.6 + , aeson <=2.1 , async ^>=2.2 , base ^>=4.16 , blaze-builder - , bytestring ^>=0.10 + , bytestring >=0.10 && <0.12 , Cabal-syntax ^>=3.8 , clock ^>=0.8 , cmark-gfm ^>=0.2 @@ -193,13 +193,13 @@ library , envparse ^>=0.5 , filepath ^>=1.4 , http-api-data ^>=0.4 - , http-client ==0.7.10 + , http-client >=0.7.10 && <0.8 , http-client-tls , http-media , http-types ^>=0.12 , iso8601-time ^>=0.1 , lens - , log-base ^>=0.11 + , log-base >=0.11 && <0.13 , log-effectful , lucid ^>=2.11 , lucid-alpine ^>=0.1 @@ -235,7 +235,7 @@ library , servant-server ^>=0.19 , servant-websockets ^>=2.0 , slugify ^>=0.1 - , souffle-haskell ^>=3.4 + , souffle-haskell >=3.4 && <3.6 , split ^>=0.2 , streaming ^>=0.2 , template-haskell @@ -287,7 +287,7 @@ executable flora-cli , effectful , effectful-core , flora - , http-client ==0.7.10 + , http-client , log-base , log-effectful , lucid @@ -325,7 +325,7 @@ test-suite flora-test , exceptions , flora , hedgehog - , http-client ==0.7.10 + , http-client , log-base , log-effectful , network-uri diff --git a/nix/envparse.nix b/nix/envparse.nix new file mode 100644 index 00000000..75994663 --- /dev/null +++ b/nix/envparse.nix @@ -0,0 +1,16 @@ +{ mkDerivation, base, containers, fetchgit, hspec, lib, text }: +mkDerivation { + pname = "envparse"; + version = "0.5.0"; + src = fetchgit { + url = "https://github.com/supki/envparse"; + sha256 = "04zxc38gk7ns9i6ycrl626804v521qxfi100iiayjhjm5hw7hjkx"; + rev = "503a699b7ec4e67e01a9216d7947b366f8025d0b"; + fetchSubmodules = true; + }; + libraryHaskellDepends = [ base containers ]; + testHaskellDepends = [ base containers hspec text ]; + homepage = "https://supki.github.io/envparse"; + description = "Parse environment variables"; + license = lib.licenses.bsd3; +} diff --git a/nix/gitignoreSource.nix b/nix/gitignoreSource.nix deleted file mode 100644 index e50725a7..00000000 --- a/nix/gitignoreSource.nix +++ /dev/null @@ -1,8 +0,0 @@ -let - owner = "hercules-ci"; - repo = "gitignore"; - rev = "c4662e662462e7bf3c2a968483478a665d00e717"; -in import (builtins.fetchTarball { - url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz"; - sha256 = "sha256:1npnx0h6bd0d7ql93ka7azhj40zgjp815fw2r6smg8ch9p7mzdlx"; -}) diff --git a/nix/log-effectful.nix b/nix/log-effectful.nix new file mode 100644 index 00000000..98158b93 --- /dev/null +++ b/nix/log-effectful.nix @@ -0,0 +1,24 @@ +{ mkDerivation, aeson, base, bytestring, effectful, effectful-core +, fetchgit, lib, log-base, tasty, tasty-hunit, text, time +, time-effectful +}: +mkDerivation { + pname = "log-effectful"; + version = "0.0.1.0"; + src = fetchgit { + url = "https://github.com/haskell-effectful/log-effectful"; + sha256 = "0nwq1i9bm29d6nh5j8sjc7m3rbs3fjf56hwph7yrgc478x645vhi"; + rev = "aaeb7eef5717e9ed26dfbf85016f277134883520"; + fetchSubmodules = true; + }; + libraryHaskellDepends = [ + aeson base bytestring effectful-core log-base text time + time-effectful + ]; + testHaskellDepends = [ + aeson base effectful effectful-core log-base tasty tasty-hunit text + time-effectful + ]; + homepage = "https://github.com/haskell-effectful/log-effectful#readme"; + license = lib.licenses.bsd3; +} diff --git a/nix/odd-jobs.nix b/nix/odd-jobs.nix new file mode 100644 index 00000000..79d10487 --- /dev/null +++ b/nix/odd-jobs.nix @@ -0,0 +1,57 @@ +{ mkDerivation, aeson, base, bytestring, containers, daemons +, directory, either, fast-logger, fetchgit, filepath, foreign-store +, friendly-time, generic-deriving, hedgehog, hostname, hpack, lib +, lifted-async, lifted-base, lucid, mmorph, monad-control +, monad-logger, mtl, optparse-applicative, postgresql-simple +, random, resource-pool, safe, servant, servant-lucid +, servant-server, servant-static-th, string-conv, tasty +, tasty-discover, tasty-hedgehog, tasty-hunit, text +, text-conversions, time, timing-convenience, unix, unliftio +, unliftio-core, unordered-containers, wai, warp +}: +mkDerivation { + pname = "odd-jobs"; + version = "0.2.2"; + src = fetchgit { + url = "https://github.com/jappeace/odd-jobs"; + sha256 = "0lxajjhvxi4sv61qhdrjiy4vfp6fc3fk3ysq7b0vqwxkz5sslwk2"; + rev = "a75515791f2c743614ec05d54493ef12b143002e"; + fetchSubmodules = true; + }; + isLibrary = true; + isExecutable = true; + libraryHaskellDepends = [ + aeson base bytestring daemons directory either fast-logger filepath + friendly-time generic-deriving hostname lucid monad-control + monad-logger mtl optparse-applicative postgresql-simple + resource-pool safe servant servant-lucid servant-server + servant-static-th string-conv text text-conversions time + timing-convenience unix unliftio unliftio-core unordered-containers + wai warp + ]; + libraryToolDepends = [ hpack ]; + executableHaskellDepends = [ + aeson base bytestring daemons directory either fast-logger filepath + foreign-store friendly-time generic-deriving hostname lucid + monad-control monad-logger mtl optparse-applicative + postgresql-simple resource-pool safe servant servant-lucid + servant-server servant-static-th string-conv text text-conversions + time timing-convenience unix unliftio unliftio-core + unordered-containers wai warp + ]; + testHaskellDepends = [ + aeson base bytestring containers daemons directory either + fast-logger filepath friendly-time generic-deriving hedgehog + hostname lifted-async lifted-base lucid mmorph monad-control + monad-logger mtl optparse-applicative postgresql-simple random + resource-pool safe servant servant-lucid servant-server + servant-static-th string-conv tasty tasty-discover tasty-hedgehog + tasty-hunit text text-conversions time timing-convenience unix + unliftio unliftio-core unordered-containers wai warp + ]; + testToolDepends = [ tasty-discover ]; + prePatch = "hpack"; + homepage = "https://www.haskelltutorials.com/odd-jobs"; + description = "A full-featured PostgreSQL-backed job queue (with an admin UI)"; + license = lib.licenses.bsd3; +} diff --git a/nix/pg-transact-effectful.nix b/nix/pg-transact-effectful.nix new file mode 100644 index 00000000..01dae55f --- /dev/null +++ b/nix/pg-transact-effectful.nix @@ -0,0 +1,18 @@ +{ mkDerivation, base, effectful-core, fetchgit, lib, mtl +, pg-transact, postgresql-simple, resource-pool +}: +mkDerivation { + pname = "pg-transact-effectful"; + version = "0.0.1.0"; + src = fetchgit { + url = "https://github.com/kleidukos/pg-transact-effectful"; + sha256 = "1ijrppsyilcf5079hdh711sdq8mc3qy1p9v6p6zvp9sxj52macj5"; + rev = "45730b124c7c21f1dcfd85667fda1c19b8ec9723"; + fetchSubmodules = true; + }; + libraryHaskellDepends = [ + base effectful-core mtl pg-transact postgresql-simple resource-pool + ]; + homepage = "https://github.com/kleidukos/pg-transact-effectful/"; + license = lib.licenses.mit; +} diff --git a/nix/pin.nix b/nix/pin.nix deleted file mode 100644 index 5b715c66..00000000 --- a/nix/pin.nix +++ /dev/null @@ -1,4 +0,0 @@ -import (builtins.fetchTarball { - url = - "https://github.com/NixOS/nixpkgs/archive/651c5cef10bee1990e92389606e8eb4162d19421.tar.gz"; -}) diff --git a/nix/pin2.nix b/nix/pin2.nix deleted file mode 100644 index 49dee80e..00000000 --- a/nix/pin2.nix +++ /dev/null @@ -1,4 +0,0 @@ -import (builtins.fetchTarball { - url = - "https://github.com/NixOS/nixpkgs/archive/61f798890ec12e5fe0adb8959070cab7c34f3fd4.tar.gz"; -}) diff --git a/nix/prometheus-client.nix b/nix/prometheus-client.nix new file mode 100644 index 00000000..ed84672d --- /dev/null +++ b/nix/prometheus-client.nix @@ -0,0 +1,33 @@ +{ mkDerivation, atomic-primops, base, bytestring, clock, containers +, criterion, data-sketches, deepseq, doctest, exceptions, fetchgit +, hspec, lib, mtl, primitive, QuickCheck, random, random-shuffle +, stm, text, transformers, transformers-compat, utf8-string +}: +mkDerivation { + pname = "prometheus-client"; + version = "1.1.0"; + src = fetchgit { + url = "https://github.com/fimad/prometheus-haskell"; + sha256 = "1xg3jyhy60xxhcwcl8sc55r7yzya0nqjl8bchms6cvfnzldrcih5"; + rev = "43f19dae23f1e374c6e99eed6840ce185cca66c1"; + fetchSubmodules = true; + }; + postUnpack = "sourceRoot+=/prometheus-client; echo source root reset to $sourceRoot"; + libraryHaskellDepends = [ + atomic-primops base bytestring clock containers data-sketches + deepseq exceptions mtl primitive stm text transformers + transformers-compat utf8-string + ]; + testHaskellDepends = [ + atomic-primops base bytestring clock containers data-sketches + deepseq doctest exceptions hspec mtl primitive QuickCheck + random-shuffle stm text transformers transformers-compat + utf8-string + ]; + benchmarkHaskellDepends = [ + base bytestring criterion random text utf8-string + ]; + homepage = "https://github.com/fimad/prometheus-haskell"; + description = "Haskell client library for http://prometheus.io."; + license = lib.licenses.asl20; +} diff --git a/nix/prometheus-metrics-ghc.nix b/nix/prometheus-metrics-ghc.nix new file mode 100644 index 00000000..a910da08 --- /dev/null +++ b/nix/prometheus-metrics-ghc.nix @@ -0,0 +1,21 @@ +{ mkDerivation, base, doctest, fetchgit, lib, prometheus-client +, text, utf8-string +}: +mkDerivation { + pname = "prometheus-metrics-ghc"; + version = "1.0.1.2"; + src = fetchgit { + url = "https://github.com/fimad/prometheus-haskell"; + sha256 = "1xg3jyhy60xxhcwcl8sc55r7yzya0nqjl8bchms6cvfnzldrcih5"; + rev = "43f19dae23f1e374c6e99eed6840ce185cca66c1"; + fetchSubmodules = true; + }; + postUnpack = "sourceRoot+=/prometheus-metrics-ghc; echo source root reset to $sourceRoot"; + libraryHaskellDepends = [ + base prometheus-client text utf8-string + ]; + testHaskellDepends = [ base doctest prometheus-client ]; + homepage = "https://github.com/fimad/prometheus-haskell"; + description = "Metrics exposing GHC runtime information for use with prometheus-client"; + license = lib.licenses.asl20; +} diff --git a/nix/raven-haskell.nix b/nix/raven-haskell.nix new file mode 100644 index 00000000..9074df3f --- /dev/null +++ b/nix/raven-haskell.nix @@ -0,0 +1,25 @@ +{ mkDerivation, aeson, base, bytestring, fetchgit, hspec +, http-conduit, lib, mtl, network, random, resourcet, text, time +, unordered-containers, uuid-types +}: +mkDerivation { + pname = "raven-haskell"; + version = "0.1.4.1"; + src = fetchgit { + url = "https://gitlab.com/dpwiz/raven-haskell"; + sha256 = "11bk7qj159glbx252a92lqdwhp2xpl9zfhqx3zhk1zbm6lrskzwk"; + rev = "9dacea2bec9c6f5d9f7d46a2a1d9094cf6147fbf"; + fetchSubmodules = true; + }; + postUnpack = "sourceRoot+=/./raven-haskell; echo source root reset to $sourceRoot"; + libraryHaskellDepends = [ + aeson base bytestring http-conduit mtl network random resourcet + text time unordered-containers uuid-types + ]; + testHaskellDepends = [ + aeson base bytestring hspec time unordered-containers + ]; + homepage = "https://bitbucket.org/dpwiz/raven-haskell"; + description = "Haskell client for Sentry logging service"; + license = lib.licenses.mit; +} diff --git a/nix/servant-effectful.nix b/nix/servant-effectful.nix new file mode 100644 index 00000000..5451d837 --- /dev/null +++ b/nix/servant-effectful.nix @@ -0,0 +1,22 @@ +{ mkDerivation, base, effectful-core, fetchgit, hashable, lib, mtl +, servant, servant-server, tasty, tasty-hunit, wai, warp +}: +mkDerivation { + pname = "servant-effectful"; + version = "0.0.1.0"; + src = fetchgit { + url = "https://github.com/kleidukos/servant-effectful"; + sha256 = "1vrp4883jsnq4rgdh89qhka6zs2q96bfxi3m1iaqvc7984g1pl64"; + rev = "65e3041c6cfbc315b20ad22ca18f61dda104eec8"; + fetchSubmodules = true; + }; + libraryHaskellDepends = [ + base effectful-core hashable mtl servant servant-server wai warp + ]; + testHaskellDepends = [ + base effectful-core hashable servant servant-server tasty + tasty-hunit + ]; + homepage = "https://github.com/haskell-effectful/servant-effectful/tree/main/servant-effectful#readme"; + license = lib.licenses.mit; +} diff --git a/nix/time-effectful.nix b/nix/time-effectful.nix new file mode 100644 index 00000000..e0079a4f --- /dev/null +++ b/nix/time-effectful.nix @@ -0,0 +1,19 @@ +{ mkDerivation, base, effectful-core, fetchgit, lib, tasty +, tasty-hunit, time +}: +mkDerivation { + pname = "time-effectful"; + version = "0.0.1.0"; + src = fetchgit { + url = "https://github.com/haskell-effectful/time-effectful"; + sha256 = "12sir7ln4nfx9w5xz77g23jlfvhnwvv4gzw20czj6vbpak8zz3i1"; + rev = "e212239b685e1ecf7ee95dd1e944cc563351907f"; + fetchSubmodules = true; + }; + libraryHaskellDepends = [ base effectful-core time ]; + testHaskellDepends = [ + base effectful-core tasty tasty-hunit time + ]; + homepage = "https://github.com/haskell-effectful/time-effectful#readme"; + license = lib.licenses.mit; +} diff --git a/nix/wai-middleware-heartbeat.nix b/nix/wai-middleware-heartbeat.nix new file mode 100644 index 00000000..bd770235 --- /dev/null +++ b/nix/wai-middleware-heartbeat.nix @@ -0,0 +1,14 @@ +{ mkDerivation, base, fetchgit, http-types, lib, wai }: +mkDerivation { + pname = "wai-middleware-heartbeat"; + version = "0.0.1.0"; + src = fetchgit { + url = "https://github.com/flora-pm/wai-middleware-heartbeat"; + sha256 = "1s2flv2jhfnd4vdfg6rmvq7s852w1pypasdg0l6ih6raaqyqzybn"; + rev = "bd7dbbe83d25c00fcd2cf5c77736af904910c596"; + fetchSubmodules = true; + }; + libraryHaskellDepends = [ base http-types wai ]; + description = "Heartbeat middleware for the WAI ecosystem"; + license = lib.licenses.mit; +} diff --git a/nix/wai-middleware-prometheus.nix b/nix/wai-middleware-prometheus.nix new file mode 100644 index 00000000..4155c8ba --- /dev/null +++ b/nix/wai-middleware-prometheus.nix @@ -0,0 +1,22 @@ +{ mkDerivation, base, bytestring, clock, data-default, doctest +, fetchgit, http-types, lib, prometheus-client, text, wai +}: +mkDerivation { + pname = "wai-middleware-prometheus"; + version = "1.0.0.1"; + src = fetchgit { + url = "https://github.com/fimad/prometheus-haskell"; + sha256 = "1xg3jyhy60xxhcwcl8sc55r7yzya0nqjl8bchms6cvfnzldrcih5"; + rev = "43f19dae23f1e374c6e99eed6840ce185cca66c1"; + fetchSubmodules = true; + }; + postUnpack = "sourceRoot+=/wai-middleware-prometheus; echo source root reset to $sourceRoot"; + libraryHaskellDepends = [ + base bytestring clock data-default http-types prometheus-client + text wai + ]; + testHaskellDepends = [ base doctest prometheus-client ]; + homepage = "https://github.com/fimad/prometheus-haskell"; + description = "WAI middlware for exposing http://prometheus.io metrics."; + license = lib.licenses.asl20; +} diff --git a/shell.nix b/shell.nix index 66b6895b..baf03e74 100644 --- a/shell.nix +++ b/shell.nix @@ -1,40 +1,7 @@ -{ -# https://github.com/NixOS/nixpkgs/blob/master/pkgs/top-level/haskell-packages.nix -pkgs ? import ./nix/pin.nix { } }: -pkgs.mkShell rec { - nativeBuildInputs = [ - pkgs.bash - pkgs.cabal-install - pkgs.cacert - pkgs.concurrently - pkgs.esbuild - pkgs.ghcid - pkgs.git - pkgs.haskellPackages.apply-refact - pkgs.haskellPackages.cabal-fmt - (pkgs.haskell.lib.markUnbroken pkgs.haskellPackages.postgresql-migration) - pkgs.hlint - pkgs.ncurses6 - pkgs.postgresql_14 - pkgs.tmux - pkgs.yarn - pkgs.libffi - pkgs.zlib - (pkgs.haskell.lib.dontCheck - (pkgs.haskell.packages."ghc921".callHackageDirect { - pkg = "fourmolu"; - ver = "0.7.0.1"; - sha256 = "0wrcmd7v0sfyagiwqxnh117xqikid3hfz2vkxzihywx0ld7jp780"; - } { })) - (import ./nix/pin2.nix { }).souffle - ]; - exactDeps = true; - NIX_PATH = "nixpkgs=${pkgs.path}:."; - shellHook = '' - export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath nativeBuildInputs}" - source environment.sh - export LOCALE_ARCHIVE="/nix/store/m53mq2077pfxhqf37gdbj7fkkdc1c8hc-glibc-locales-2.27/lib/locale/locale-archive" - export LC_ALL=C.UTF-8 - cat scripts/shell-welcome.txt - ''; -} +(import ( + fetchTarball { + url = "https://github.com/edolstra/flake-compat/archive/b4a34015c698c7793d592d66adbab377907a2be8.tar.gz"; + sha256 = "1qc703yg0babixi6wshn5wm2kgl5y1drcswgszh4xxzbrwkk9sv7"; } +) { + src = ./.; +}).shellNix From 49af162eb7febb88bd15600ab374c7212f7249f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Mon, 10 Oct 2022 21:29:42 +0200 Subject: [PATCH 10/29] [NO-ISSUE] Update log-effectful to 1.0.0.0 --- app/cli/Main.hs | 2 +- cabal.project | 11 -- cabal.project.freeze | 6 +- default.nix | 12 +- flake.nix | 278 ++++++++++++++---------------- flora.cabal | 15 +- shell.nix | 12 +- src/Flora/Import/Package.hs | 12 +- src/Flora/Import/Package/Bulk.hs | 2 +- src/Flora/Model/Package/Query.hs | 43 +++-- src/Flora/OddJobs.hs | 5 +- src/Flora/OddJobs/Types.hs | 21 ++- src/Flora/Publish.hs | 6 +- src/FloraWeb/Server/Auth.hs | 6 +- src/FloraWeb/Server/Auth/Types.hs | 8 +- src/FloraWeb/Server/Logging.hs | 18 +- src/FloraWeb/Types.hs | 4 +- src/Log/Backend/File.hs | 9 +- test/Flora/TestUtils.hs | 20 ++- test/Main.hs | 9 +- 20 files changed, 232 insertions(+), 267 deletions(-) diff --git a/app/cli/Main.hs b/app/cli/Main.hs index aa3a0766..5b9e0343 100644 --- a/app/cli/Main.hs +++ b/app/cli/Main.hs @@ -5,10 +5,10 @@ import Data.Password.Types import Data.Text (Text) import DesignSystem (generateComponents) import Effectful -import Effectful.Log.Backend.StandardOutput qualified as Log import Effectful.PostgreSQL.Transact.Effect import Flora.Model.User.Query qualified as Query import GHC.Generics (Generic) +import Log.Backend.StandardOutput qualified as Log import Optics.Core import Options.Applicative diff --git a/cabal.project b/cabal.project index 7f702dad..847f0e8b 100644 --- a/cabal.project +++ b/cabal.project @@ -41,12 +41,6 @@ source-repository-package location: https://github.com/flora-pm/wai-middleware-heartbeat tag: bd7dbbe -source-repository-package - type: git - location: https://github.com/scrive/log - tag: 73b4735 - subdir: ./log-base - -- need to use jappeace until this is merged -- (provides resource-pool 3 support) -- https://github.com/saurabhnanda/odd-jobs/pull/90 @@ -63,11 +57,6 @@ source-repository-package type: git location: https://github.com/kleidukos/servant-effectful -source-repository-package - type: git - location: https://github.com/haskell-effectful/log-effectful - tag: aaeb7ee - source-repository-package type: git location: https://github.com/haskell-effectful/time-effectful diff --git a/cabal.project.freeze b/cabal.project.freeze index 5f947d08..86aa7ad3 100644 --- a/cabal.project.freeze +++ b/cabal.project.freeze @@ -180,8 +180,8 @@ constraints: any.Cabal ==3.6.3.0, any.lens-aeson ==1.2.2, any.lifted-async ==0.10.2.3, any.lifted-base ==0.2.3.12, - any.log-base ==0.11.1.0, - any.log-effectful ==0.0.1.0, + any.log-base ==0.12.0.0, + any.log-effectful ==1.0.0.0, any.lucid ==2.11.1, any.lucid-alpine ==0.1.0.7, any.lucid-aria ==0.1.0.1, @@ -388,4 +388,4 @@ constraints: any.Cabal ==3.6.3.0, any.xml-types ==0.3.8, any.zlib ==0.6.3.0, zlib -bundled-c-zlib -non-blocking-ffi -pkg-config -index-state: hackage.haskell.org 2022-09-10T22:52:40Z +index-state: hackage.haskell.org 2022-10-10T16:14:10Z diff --git a/default.nix b/default.nix index 8736ee96..45f89ea5 100644 --- a/default.nix +++ b/default.nix @@ -1,7 +1,5 @@ -(import ( - fetchTarball { - url = "https://github.com/edolstra/flake-compat/archive/b4a34015c698c7793d592d66adbab377907a2be8.tar.gz"; - sha256 = "1qc703yg0babixi6wshn5wm2kgl5y1drcswgszh4xxzbrwkk9sv7"; } -) { - src = ./.; -}).defaultNix +(import (fetchTarball { + url = + "https://github.com/edolstra/flake-compat/archive/b4a34015c698c7793d592d66adbab377907a2be8.tar.gz"; + sha256 = "1qc703yg0babixi6wshn5wm2kgl5y1drcswgszh4xxzbrwkk9sv7"; +}) { src = ./.; }).defaultNix diff --git a/flake.nix b/flake.nix index 55b50716..f30d5be8 100644 --- a/flake.nix +++ b/flake.nix @@ -1,7 +1,8 @@ -{ inputs = { +{ + inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; - utils.url = github:numtide/flake-utils; + utils.url = "github:numtide/flake-utils"; }; outputs = { nixpkgs, utils, ... }: @@ -9,168 +10,143 @@ let compiler = "ghc92"; - config = { allowBroken = true; allowUnsupportedSystem = true; }; + config = { + allowBroken = true; + allowUnsupportedSystem = true; + }; overlay = pkgsNew: pkgsOld: { - flora = - pkgsNew.haskell.lib.justStaticExecutables - (pkgsNew.overrideCabal - pkgsNew.haskellPackages.flora + flora = pkgsNew.haskell.lib.justStaticExecutables + (pkgsNew.overrideCabal pkgsNew.haskellPackages.flora (old: { + nativeBuildInputs = (old.nativeBuildInputs or [ ]) + ++ [ pkgsNew.makeWrapper ]; + + postInstall = (old.postInstall or "") ++ '' + wrapProgram $out/bin/flora-cli --prefix PATH : ${pkgsNew.souffle}/bin + ''; + })); + + haskell = pkgsOld.haskell // { + packages = pkgsOld.haskell.packages // { + "${compiler}" = pkgsOld.haskell.packages."${compiler}".override (old: { - nativeBuildInputs = (old.nativeBuildInputs or []) ++ [ - pkgsNew.makeWrapper - ]; - - postInstall = (old.postInstall or "") ++ - '' - wrapProgram $out/bin/flora-cli --prefix PATH : ${pkgsNew.souffle}/bin - ''; - }) - ); - - haskell = - pkgsOld.haskell // { - packages = pkgsOld.haskell.packages // { - "${compiler}" = - pkgsOld.haskell.packages."${compiler}".override (old: { - overrides = - pkgsNew.lib.fold - pkgsNew.lib.composeExtensions - (old.overrides or (_: _: { })) - [ (pkgsNew.haskell.lib.packageSourceOverrides { - flora = ./.; - - text-display = "0.0.2.0"; - }) - (pkgsNew.haskell.lib.packagesFromDirectory { - directory = ./nix; - }) - (haskellPackagesNew: haskellPackagesOld: { - Cabal-syntax = haskellPackagesNew.Cabal_3_8_1_0; - - lens-aeson = haskellPackagesNew.lens-aeson_1_2_2; - - log-effectful = - pkgsNew.haskell.lib.doJailbreak - haskellPackagesOld.log-effectful; - - monad-time = haskellPackagesNew.monad-time_0_4_0_0; - - odd-jobs = - pkgsNew.haskell.lib.overrideCabal - haskellPackagesOld.odd-jobs - (old: { - doCheck = false; - - prePatch = ""; - - libraryToolDepends = []; - }); - - PyF = haskellPackagesNew.PyF_0_11_1_0; - - pcre2 = - pkgsNew.haskell.lib.dontCheck - haskellPackagesOld.pcre2; - - pg-entity = - pkgsNew.haskell.lib.doJailbreak - (pkgsNew.haskell.lib.dontCheck - haskellPackagesOld.pg-entity - ); - - postgresql-simple-migration = - pkgsNew.haskell.lib.doJailbreak - haskellPackagesOld.postgresql-simple-migration; - - raven-haskell = - pkgsNew.haskell.lib.dontCheck - haskellPackagesOld.raven-haskell; - - resource-pool = - haskellPackagesNew.resource-pool_0_3_1_0; - - servant-static-th = - pkgsNew.haskell.lib.dontCheck - haskellPackagesOld.servant-static-th; - - slugify = - pkgsNew.haskell.lib.dontCheck - haskellPackagesOld.slugify; - - souffle-haskell = - pkgsNew.haskell.lib.dontCheck - haskellPackagesOld.souffle-haskell; - - pg-transact = - pkgsNew.haskell.lib.dontCheck - haskellPackagesOld.pg-transact; - - pg-transact-effectful = - pkgsNew.haskell.lib.doJailbreak - haskellPackagesOld.pg-transact-effectful; - - prometheus-proc = - pkgsNew.haskell.lib.doJailbreak - haskellPackagesOld.prometheus-proc; - - text-metrics = - pkgsNew.haskell.lib.doJailbreak - haskellPackagesOld.text-metrics; - - type-errors-pretty = - pkgsNew.haskell.lib.doJailbreak - (pkgsNew.haskell.lib.dontCheck - haskellPackagesOld.type-errors-pretty - ); - - wai-middleware-heartbeat = - pkgsNew.haskell.lib.doJailbreak - haskellPackagesOld.wai-middleware-heartbeat; - - vector = - pkgsNew.haskell.lib.overrideCabal - haskellPackagesNew.vector_0_13_0_0 - (old: { - testHaskellDepends = (old.testHaskellDepends or []) ++ [ - haskellPackagesNew.doctest - ]; - }); - - vector-algorithms = - haskellPackagesNew.vector-algorithms_0_9_0_1; - }) - ]; - }); - }; + overrides = pkgsNew.lib.fold pkgsNew.lib.composeExtensions + (old.overrides or (_: _: { })) [ + (pkgsNew.haskell.lib.packageSourceOverrides { + flora = ./.; + + text-display = "0.0.2.0"; + }) + (pkgsNew.haskell.lib.packagesFromDirectory { + directory = ./nix; + }) + (haskellPackagesNew: haskellPackagesOld: { + Cabal-syntax = haskellPackagesNew.Cabal_3_8_1_0; + + lens-aeson = haskellPackagesNew.lens-aeson_1_2_2; + + log-effectful = pkgsNew.haskell.lib.doJailbreak + haskellPackagesOld.log-effectful; + + monad-time = haskellPackagesNew.monad-time_0_4_0_0; + + odd-jobs = pkgsNew.haskell.lib.overrideCabal + haskellPackagesOld.odd-jobs (old: { + doCheck = false; + + prePatch = ""; + + libraryToolDepends = [ ]; + }); + + PyF = haskellPackagesNew.PyF_0_11_1_0; + + pcre2 = pkgsNew.haskell.lib.dontCheck + haskellPackagesOld.pcre2; + + pg-entity = pkgsNew.haskell.lib.doJailbreak + (pkgsNew.haskell.lib.dontCheck + haskellPackagesOld.pg-entity); + + postgresql-simple-migration = + pkgsNew.haskell.lib.doJailbreak + haskellPackagesOld.postgresql-simple-migration; + + raven-haskell = pkgsNew.haskell.lib.dontCheck + haskellPackagesOld.raven-haskell; + + resource-pool = + haskellPackagesNew.resource-pool_0_3_1_0; + + servant-static-th = pkgsNew.haskell.lib.dontCheck + haskellPackagesOld.servant-static-th; + + slugify = pkgsNew.haskell.lib.dontCheck + haskellPackagesOld.slugify; + + souffle-haskell = pkgsNew.haskell.lib.dontCheck + haskellPackagesOld.souffle-haskell; + + pg-transact = pkgsNew.haskell.lib.dontCheck + haskellPackagesOld.pg-transact; + + pg-transact-effectful = pkgsNew.haskell.lib.doJailbreak + haskellPackagesOld.pg-transact-effectful; + + prometheus-proc = pkgsNew.haskell.lib.doJailbreak + haskellPackagesOld.prometheus-proc; + + text-metrics = pkgsNew.haskell.lib.doJailbreak + haskellPackagesOld.text-metrics; + + type-errors-pretty = pkgsNew.haskell.lib.doJailbreak + (pkgsNew.haskell.lib.dontCheck + haskellPackagesOld.type-errors-pretty); + + wai-middleware-heartbeat = + pkgsNew.haskell.lib.doJailbreak + haskellPackagesOld.wai-middleware-heartbeat; + + vector = pkgsNew.haskell.lib.overrideCabal + haskellPackagesNew.vector_0_13_0_0 (old: { + testHaskellDepends = (old.testHaskellDepends or [ ]) + ++ [ haskellPackagesNew.doctest ]; + }); + + vector-algorithms = + haskellPackagesNew.vector-algorithms_0_9_0_1; + }) + ]; + }); }; + }; }; - pkgs = - import nixpkgs { inherit config system; overlays = [ overlay ]; }; + pkgs = import nixpkgs { + inherit config system; + overlays = [ overlay ]; + }; - in - rec { - packages.default = pkgs.haskell.packages."${compiler}".flora; + in rec { + packages.default = pkgs.haskell.packages."${compiler}".flora; - apps = rec { - default = server; + apps = rec { + default = server; - server = { - type = "app"; + server = { + type = "app"; - program = "${pkgs.flora}/bin/flora-server"; - }; + program = "${pkgs.flora}/bin/flora-server"; + }; - cli = { - type = "app"; + cli = { + type = "app"; - program = "${pkgs.flora}/bin/flora-cli"; - }; + program = "${pkgs.flora}/bin/flora-cli"; }; + }; - devShells.default = pkgs.haskell.packages."${compiler}".flora.env; - } - ); + devShells.default = pkgs.haskell.packages."${compiler}".flora.env; + }); } diff --git a/flora.cabal b/flora.cabal index f4d81694..0b0abc75 100644 --- a/flora.cabal +++ b/flora.cabal @@ -33,6 +33,7 @@ common common-extensions DuplicateRecordFields LambdaCase OverloadedLabels + OverloadedRecordDot OverloadedStrings PolyKinds QuasiQuotes @@ -41,7 +42,6 @@ common common-extensions TypeFamilies UndecidableInstances ViewPatterns - OverloadedRecordDot default-language: GHC2021 @@ -171,11 +171,11 @@ library Lucid.Orphans build-depends: - , aeson <=2.1 + , aeson <2.1.0 , async ^>=2.2 , base ^>=4.16 , blaze-builder - , bytestring >=0.10 && <0.12 + , bytestring >=0.10 && <0.12 , Cabal-syntax ^>=3.8 , clock ^>=0.8 , cmark-gfm ^>=0.2 @@ -193,14 +193,14 @@ library , envparse ^>=0.5 , filepath ^>=1.4 , http-api-data ^>=0.4 - , http-client >=0.7.10 && <0.8 + , http-client >=0.7.10 && <0.8 , http-client-tls , http-media , http-types ^>=0.12 , iso8601-time ^>=0.1 , lens - , log-base >=0.11 && <0.13 - , log-effectful + , log-base >=0.12 && <0.13 + , log-effectful ^>=1.0 , lucid ^>=2.11 , lucid-alpine ^>=0.1 , lucid-aria ^>=0.1 @@ -235,7 +235,7 @@ library , servant-server ^>=0.19 , servant-websockets ^>=2.0 , slugify ^>=0.1 - , souffle-haskell >=3.4 && <3.6 + , souffle-haskell >=3.4 && <3.6 , split ^>=0.2 , streaming ^>=0.2 , template-haskell @@ -246,6 +246,7 @@ library , transformers ^>=0.5 , transformers-base ^>=0.4 , typed-process ^>=0.2 + , unliftio-core , unordered-containers ^>=0.2 , uuid ^>=1.3 , vector ^>=0.13 diff --git a/shell.nix b/shell.nix index baf03e74..754affb7 100644 --- a/shell.nix +++ b/shell.nix @@ -1,7 +1,5 @@ -(import ( - fetchTarball { - url = "https://github.com/edolstra/flake-compat/archive/b4a34015c698c7793d592d66adbab377907a2be8.tar.gz"; - sha256 = "1qc703yg0babixi6wshn5wm2kgl5y1drcswgszh4xxzbrwkk9sv7"; } -) { - src = ./.; -}).shellNix +(import (fetchTarball { + url = + "https://github.com/edolstra/flake-compat/archive/b4a34015c698c7793d592d66adbab377907a2be8.tar.gz"; + sha256 = "1qc703yg0babixi6wshn5wm2kgl5y1drcswgszh4xxzbrwkk9sv7"; +}) { src = ./.; }).shellNix diff --git a/src/Flora/Import/Package.hs b/src/Flora/Import/Package.hs index c361fbfb..aed5990e 100644 --- a/src/Flora/Import/Package.hs +++ b/src/Flora/Import/Package.hs @@ -49,7 +49,6 @@ import Distribution.Types.TestSuite import Distribution.Utils.ShortText qualified as Cabal import Effectful import Effectful.Internal.Monad (unsafeEff_) -import Effectful.Log (Logging) import Effectful.PostgreSQL.Transact.Effect (DB) import Effectful.Time (Time) import GHC.Generics (Generic) @@ -58,6 +57,7 @@ import Optics.Core import System.Directory qualified as System import System.FilePath +import Effectful.Log (Log) import Flora.Import.Categories.Tuning qualified as Tuning import Flora.Import.Types import Flora.Model.Category.Update qualified as Update @@ -133,21 +133,21 @@ coreLibraries = * finally, inserting that data into the database -} importFile - :: ([DB, IOE, Logging, Time] :>> es) + :: ([DB, IOE, Log, Time] :>> es) => UserId -> FilePath -- ^ The absolute path to the Cabal file -> Eff es () importFile userId path = loadFile path >>= extractPackageDataFromCabal userId >>= persistImportOutput -importRelFile :: ([DB, IOE, Logging, Time] :>> es) => UserId -> FilePath -> Eff es () +importRelFile :: ([DB, IOE, Log, Time] :>> es) => UserId -> FilePath -> Eff es () importRelFile user dir = do workdir <- ( dir) <$> liftIO System.getCurrentDirectory importFile user workdir -- | Loads and parses a Cabal file loadFile - :: ([DB, IOE, Logging, Time] :>> es) + :: ([DB, IOE, Log, Time] :>> es) => FilePath -- ^ The absolute path to the Cabal file -> Eff es GenericPackageDescription @@ -161,7 +161,7 @@ loadFile path = do parseString parseGenericPackageDescription path content parseString - :: (HasCallStack, [Logging, Time] :>> es) + :: (HasCallStack, [Log, Time] :>> es) => (BS.ByteString -> ParseResult a) -- ^ File contents to final value parser -> String @@ -176,7 +176,7 @@ parseString parser name bs = do Log.logAttention_ (display $ show err) throw $ CabalFileCouldNotBeParsed name -loadAndExtractCabalFile :: ([DB, IOE, Logging, Time] :>> es) => UserId -> FilePath -> Eff es ImportOutput +loadAndExtractCabalFile :: ([DB, IOE, Log, Time] :>> es) => UserId -> FilePath -> Eff es ImportOutput loadAndExtractCabalFile userId filePath = loadFile filePath >>= extractPackageDataFromCabal userId {-| Persists an 'ImportOutput' to the database. An 'ImportOutput' can be obtained diff --git a/src/Flora/Import/Package/Bulk.hs b/src/Flora/Import/Package/Bulk.hs index a7c07ff9..35b80172 100644 --- a/src/Flora/Import/Package/Bulk.hs +++ b/src/Flora/Import/Package/Bulk.hs @@ -37,7 +37,7 @@ importAllFilesInDirectory appLogger user dir = do let chunkSize = 400 countMVar <- liftIO $ newMVar @Int 0 findAllCabalFilesInDirectory dir - & parMapM parallelWorkers (runEff . runDB pool . runCurrentTimeIO . Log.runLogging "flora-jobs" appLogger defaultLogLevel . loadAndExtractCabalFile user) + & parMapM parallelWorkers (runEff . runDB pool . runCurrentTimeIO . Log.runLog "flora-jobs" appLogger defaultLogLevel . loadAndExtractCabalFile user) & chunksOf chunkSize & Str.mapped Str.toList & Str.mapM_ (persistChunk countMVar) diff --git a/src/Flora/Model/Package/Query.hs b/src/Flora/Model/Package/Query.hs index 8a6fdf61..81f96410 100644 --- a/src/Flora/Model/Package/Query.hs +++ b/src/Flora/Model/Package/Query.hs @@ -28,7 +28,6 @@ import Effectful (Eff, IOE, type (:>>)) import Effectful.Log import Effectful.PostgreSQL.Transact.Effect import Effectful.Time -import Log (object, (.=)) import Log qualified import Flora.Model.Category (Category, CategoryId) @@ -42,7 +41,7 @@ import Flora.Model.Package.Component import Flora.Model.Release.Types (ReleaseId) import FloraWeb.Server.Logging (timeAction) -getAllPackages :: ([DB, Logging, Time, IOE] :>> es) => Eff es (Vector Package) +getAllPackages :: ([DB, Log, Time, IOE] :>> es) => Eff es (Vector Package) getAllPackages = do (result, duration) <- timeAction $ dbtToEff $ query_ Select (_select @Package) Log.logInfo "Retrieving all packages" $ @@ -50,10 +49,10 @@ getAllPackages = do ["duration" .= duration] pure result -getPackagesByNamespace :: ([DB, Logging, Time, IOE] :>> es) => Namespace -> Eff es (Vector Package) +getPackagesByNamespace :: ([DB, Log, Time, IOE] :>> es) => Namespace -> Eff es (Vector Package) getPackagesByNamespace namespace = dbtToEff $ selectManyByField @Package [field| namespace |] (Only namespace) -getPackageByNamespaceAndName :: ([DB, Logging, Time, IOE] :>> es) => Namespace -> PackageName -> Eff es (Maybe Package) +getPackageByNamespaceAndName :: ([DB, Log, Time, IOE] :>> es) => Namespace -> PackageName -> Eff es (Maybe Package) getPackageByNamespaceAndName namespace name = do (result, duration) <- timeAction $ @@ -70,7 +69,7 @@ getPackageByNamespaceAndName namespace name = do pure result -- | This function is to be used when in Hackage Compatibility Mode. -getHaskellOrHackagePackage :: ([DB, Logging, Time, IOE] :>> es) => PackageName -> Eff es (Maybe Package) +getHaskellOrHackagePackage :: ([DB, Log, Time, IOE] :>> es) => PackageName -> Eff es (Maybe Package) getHaskellOrHackagePackage packageName = dbtToEff $ queryOne @@ -90,19 +89,19 @@ getHaskellOrHackagePackage packageName = -- | TODO: Remove the manual fields and use pg-entity getAllPackageDependents - :: ([DB, Logging, Time, IOE] :>> es) + :: ([DB, Log, Time, IOE] :>> es) => Namespace -> PackageName -> Eff es (Vector Package) getAllPackageDependents namespace packageName = dbtToEff $ query Select packageDependentsQuery (namespace, packageName) -- | This function gets the first 6 dependents of a package -getPackageDependents :: ([DB, Logging, Time, IOE] :>> es) => Namespace -> PackageName -> Eff es (Vector Package) +getPackageDependents :: ([DB, Log, Time, IOE] :>> es) => Namespace -> PackageName -> Eff es (Vector Package) getPackageDependents namespace packageName = dbtToEff $ query Select q (namespace, packageName) where q = packageDependentsQuery <> " LIMIT 6" -getNumberOfPackageDependents :: ([DB, Logging, Time, IOE] :>> es) => Namespace -> PackageName -> Eff es Word +getNumberOfPackageDependents :: ([DB, Log, Time, IOE] :>> es) => Namespace -> PackageName -> Eff es Word getNumberOfPackageDependents namespace packageName = dbtToEff $ do (result :: Maybe (Only Int)) <- queryOne Select numberOfPackageDependentsQuery (namespace, packageName) case result of @@ -138,7 +137,7 @@ packageDependentsQuery = |] getAllPackageDependentsWithLatestVersion - :: ([DB, Logging, Time, IOE] :>> es) + :: ([DB, Log, Time, IOE] :>> es) => Namespace -> PackageName -> Eff es (Vector (Namespace, PackageName, Text, Version)) @@ -147,7 +146,7 @@ getAllPackageDependentsWithLatestVersion namespace packageName = query Select packageDependentsWithLatestVersionQuery (namespace, packageName) getPackageDependentsWithLatestVersion - :: ([DB, Logging, Time, IOE] :>> es) + :: ([DB, Log, Time, IOE] :>> es) => Namespace -> PackageName -> Eff es (Vector (Namespace, PackageName, Text, Version)) @@ -180,10 +179,10 @@ packageDependentsWithLatestVersionQuery = GROUP BY (p.namespace, p.name, synopsis) |] -getComponentById :: ([DB, Logging, Time, IOE] :>> es) => ComponentId -> Eff es (Maybe PackageComponent) +getComponentById :: ([DB, Log, Time, IOE] :>> es) => ComponentId -> Eff es (Maybe PackageComponent) getComponentById componentId = dbtToEff $ selectById @PackageComponent (Only componentId) -getComponent :: ([DB, Logging, Time, IOE] :>> es) => ReleaseId -> Text -> ComponentType -> Eff es (Maybe PackageComponent) +getComponent :: ([DB, Log, Time, IOE] :>> es) => ReleaseId -> Text -> ComponentType -> Eff es (Maybe PackageComponent) getComponent releaseId name componentType = dbtToEff $ queryOne Select (_selectWhere @PackageComponent queryFields) (releaseId, name, componentType) @@ -196,7 +195,7 @@ getComponent releaseId name componentType = ] unsafeGetComponent - :: ([DB, Logging, Time, IOE] :>> es) + :: ([DB, Log, Time, IOE] :>> es) => ReleaseId -> Eff es (Maybe PackageComponent) unsafeGetComponent releaseId = @@ -207,14 +206,14 @@ unsafeGetComponent releaseId = queryFields = [[field| release_id |]] getAllRequirements - :: ([DB, Logging, Time, IOE] :>> es) + :: ([DB, Log, Time, IOE] :>> es) => ReleaseId -- ^ Id of the release for which we want the dependencies -> Eff es (Vector (Namespace, PackageName, Text, Version, Text)) -- ^ Returns a vector of (Namespace, Name, dependency requirement, version of latest of release of dependency, synopsis of dependency) getAllRequirements releaseId = dbtToEff $ query Select getAllRequirementsQuery (Only releaseId) -getRequirements :: ([DB, Logging, Time, IOE] :>> es) => ReleaseId -> Eff es (Vector (Namespace, PackageName, Text)) +getRequirements :: ([DB, Log, Time, IOE] :>> es) => ReleaseId -> Eff es (Vector (Namespace, PackageName, Text)) getRequirements releaseId = do (result, duration) <- timeAction $ dbtToEff $ query Select (getRequirementsQuery <> " LIMIT 6") (Only releaseId) Log.logInfo "Retrieving limited dependencies of a release" $ @@ -267,7 +266,7 @@ getRequirementsQuery = order by dependency.namespace desc |] -getNumberOfPackageRequirements :: ([DB, Logging, Time, IOE] :>> es) => ReleaseId -> Eff es Word +getNumberOfPackageRequirements :: ([DB, Log, Time, IOE] :>> es) => ReleaseId -> Eff es Word getNumberOfPackageRequirements releaseId = dbtToEff $ do (result :: Maybe (Only Int)) <- queryOne Select numberOfPackageRequirementsQuery (Only releaseId) case result of @@ -286,7 +285,7 @@ numberOfPackageRequirementsQuery = |] getPackageCategories - :: ([DB, Logging, Time, IOE] :>> es) + :: ([DB, Log, Time, IOE] :>> es) => PackageId -> Eff es (Vector Category) getPackageCategories packageId = @@ -298,7 +297,7 @@ getPackageCategories packageId = packageId getPackagesFromCategoryWithLatestVersion - :: ([DB, Logging, Time, IOE] :>> es) + :: ([DB, Log, Time, IOE] :>> es) => CategoryId -> Eff es (Vector (Namespace, PackageName, Text, Version)) getPackagesFromCategoryWithLatestVersion categoryId = dbtToEff $ query Select q (Only categoryId) @@ -312,7 +311,7 @@ getPackagesFromCategoryWithLatestVersion categoryId = dbtToEff $ query Select q |] searchPackage - :: ([DB, Logging, Time, IOE] :>> es) + :: ([DB, Log, Time, IOE] :>> es) => Word -> Text -> Eff es (Vector (Namespace, PackageName, Text, Version, Float)) @@ -343,7 +342,7 @@ searchPackage pageNumber searchString = (searchString, searchString, offset) listAllPackages - :: ([DB, Logging, Time, IOE] :>> es) + :: ([DB, Log, Time, IOE] :>> es) => Word -> Eff es (Vector (Namespace, PackageName, Text, Version, Float)) listAllPackages pageNumber = @@ -371,7 +370,7 @@ listAllPackages pageNumber = |] (Only offset) -countPackages :: ([DB, Logging, Time, IOE] :>> es) => Eff es Word +countPackages :: ([DB, Log, Time, IOE] :>> es) => Eff es Word countPackages = dbtToEff $ do (result :: Maybe (Only Int)) <- queryOne_ @@ -385,7 +384,7 @@ countPackages = dbtToEff $ do Just (Only n) -> pure $ fromIntegral n Nothing -> pure 0 -countPackagesByName :: ([DB, Logging, Time, IOE] :>> es) => Text -> Eff es Word +countPackagesByName :: ([DB, Log, Time, IOE] :>> es) => Text -> Eff es Word countPackagesByName searchString = dbtToEff $ do (result :: Maybe (Only Int)) <- queryOne diff --git a/src/Flora/OddJobs.hs b/src/Flora/OddJobs.hs index 86e8c438..b0c90b85 100644 --- a/src/Flora/OddJobs.hs +++ b/src/Flora/OddJobs.hs @@ -36,7 +36,6 @@ import Database.PostgreSQL.Simple (Only (..)) import Database.PostgreSQL.Simple qualified as PG import Database.PostgreSQL.Simple.SqlQQ (sql) import Distribution.Types.Version -import Effectful.Log (localDomainEff', logMessageEff') import Effectful.PostgreSQL.Transact.Effect import Log import Lucid qualified @@ -182,9 +181,9 @@ fetchNewIndex = localDomain "index-import" $ do liftIO $ void $ scheduleIndexImportJob pool runner :: Job -> JobsRunner () -runner job = localDomainEff' "job-runner" $ +runner job = localDomain "job-runner" $ case fromJSON (jobPayload job) of - Error str -> logMessageEff' LogAttention "decode error" (toJSON str) + Error str -> logMessage LogAttention "decode error" (toJSON str) Success val -> case val of MkReadme x -> makeReadme x FetchUploadTime x -> fetchUploadTime x diff --git a/src/Flora/OddJobs/Types.hs b/src/Flora/OddJobs/Types.hs index 5f0432f7..1304bd0b 100644 --- a/src/Flora/OddJobs/Types.hs +++ b/src/Flora/OddJobs/Types.hs @@ -14,12 +14,11 @@ import Database.PostgreSQL.Simple.Types (QualifiedIdentifier) import Distribution.Pretty import Distribution.Version (Version, mkVersion, versionNumbers) import Effectful -import Effectful.Log -import Effectful.Log qualified as LogEff +import Effectful.Log hiding (LogLevel) +import Effectful.Log qualified as LogEff hiding (LogLevel) import Effectful.Reader.Static (Reader, runReader) import GHC.Generics (Generic) import GHC.Stack (HasCallStack, callStack, prettyCallStack) -import Log hiding (LogLevel (..)) import Log qualified import Network.HTTP.Client import OddJobs.Job (Job, LogEvent (..), LogLevel (..)) @@ -39,7 +38,7 @@ type JobsRunner = Eff '[ DB , Reader JobsRunnerEnv - , Logging + , Log , Time , IOE ] @@ -48,7 +47,7 @@ runJobRunner :: Pool Connection -> JobsRunnerEnv -> Logger -> JobsRunner a -> IO runJobRunner pool runnerEnv logger jobRunner = runEff . runCurrentTimeIO - . LogEff.runLogging "flora-jobs" logger defaultLogLevel + . LogEff.runLog "flora-jobs" logger defaultLogLevel . runReader runnerEnv . runDB pool $ jobRunner @@ -138,10 +137,10 @@ structuredLogging FloraConfig{..} logger level event = runEff . runCurrentTimeIO . Logging.runLog environment logger - $ localDomainEff' "odd-jobs" + $ localDomain "odd-jobs" $ case level of - LevelDebug -> logMessageEff' Log.LogTrace "LevelDebug" (toJSON event) - LevelInfo -> logMessageEff' Log.LogInfo "LevelInfo" (toJSON event) - LevelWarn -> logMessageEff' Log.LogAttention "LevelWarn" (toJSON event) - LevelError -> logMessageEff' Log.LogAttention "LevelError" (toJSON event) - (LevelOther x) -> logMessageEff' Log.LogAttention ("LevelOther " <> Text.pack (show x)) (toJSON event) + LevelDebug -> logMessage Log.LogTrace "LevelDebug" (toJSON event) + LevelInfo -> logMessage Log.LogInfo "LevelInfo" (toJSON event) + LevelWarn -> logMessage Log.LogAttention "LevelWarn" (toJSON event) + LevelError -> logMessage Log.LogAttention "LevelError" (toJSON event) + (LevelOther x) -> logMessage Log.LogAttention ("LevelOther " <> Text.pack (show x)) (toJSON event) diff --git a/src/Flora/Publish.hs b/src/Flora/Publish.hs index c3a6699c..3023a28e 100644 --- a/src/Flora/Publish.hs +++ b/src/Flora/Publish.hs @@ -24,7 +24,7 @@ import Flora.Model.Requirement (Requirement) TODO: Publish artifacts -} publishPackage - :: ([DB, Logging, Time, IOE] :>> es) + :: ([DB, Log, Time, IOE] :>> es) => [Requirement] -> [PackageComponent] -> Release @@ -42,7 +42,7 @@ publishPackage requirements components release userPackageCategories package = d liftIO $ T.putStrLn $ "[+] Package " <> display (package.name) <> " does not exist." publishForNewPackage requirements components release userPackageCategories package -publishForExistingPackage :: ([DB, Logging, IOE] :>> es) => [Requirement] -> [PackageComponent] -> Release -> Package -> Eff es Package +publishForExistingPackage :: ([DB, Log, IOE] :>> es) => [Requirement] -> [PackageComponent] -> Release -> Package -> Eff es Package publishForExistingPackage requirements components release package = do result <- Query.getReleaseByVersion (package.packageId) (release.version) case result of @@ -66,7 +66,7 @@ publishForExistingPackage requirements components release package = do liftIO $ T.putStrLn $ "[+] I am not inserting anything for " <> display (package.name) <> " v" <> display (r.version) pure package -publishForNewPackage :: ([DB, Logging, IOE] :>> es) => [Requirement] -> [PackageComponent] -> Release -> [UserPackageCategory] -> Package -> Eff es Package +publishForNewPackage :: ([DB, Log, IOE] :>> es) => [Requirement] -> [PackageComponent] -> Release -> [UserPackageCategory] -> Package -> Eff es Package publishForNewPackage requirements components release userPackageCategories package = do liftIO $ T.putStrLn $ "[+] Normalising user-supplied categories: " <> display userPackageCategories newCategories <- liftIO $ (.normalisedCategories) <$> Tuning.normalise userPackageCategories diff --git a/src/FloraWeb/Server/Auth.hs b/src/FloraWeb/Server/Auth.hs index ba663885..9cac0e4b 100644 --- a/src/FloraWeb/Server/Auth.hs +++ b/src/FloraWeb/Server/Auth.hs @@ -11,7 +11,7 @@ import Data.Text (Text) import Data.UUID qualified as UUID import Effectful import Effectful.Error.Static (Error, throwError) -import Effectful.Log (Logging) +import Effectful.Log (Log) import Effectful.PostgreSQL.Transact.Effect (DB) import Effectful.PostgreSQL.Transact.Effect qualified as DB import Effectful.Servant (handlerToEff) @@ -48,7 +48,7 @@ authHandler logger floraEnv = & Servant.effToHandler ) where - handler :: Request -> Eff '[Logging, DB, IsVisitor, Error ServerError, IOE] (Headers '[Header "Set-Cookie" SetCookie] Session) + handler :: Request -> Eff '[Log, DB, IsVisitor, Error ServerError, IOE] (Headers '[Header "Set-Cookie" SetCookie] Session) handler req = do let cookies = getCookies req mbPersistentSessionId <- handlerToEff $ getSessionId cookies @@ -89,7 +89,7 @@ getSessionId cookies = Just sessionId -> pure $ Just sessionId getInTheFuckingSessionShinji - :: ([Logging, DB, IOE] :>> es) + :: ([Log, DB, IOE] :>> es) => Maybe PersistentSessionId -> Eff es (Maybe PersistentSession) getInTheFuckingSessionShinji Nothing = pure Nothing diff --git a/src/FloraWeb/Server/Auth/Types.hs b/src/FloraWeb/Server/Auth/Types.hs index 10cf7921..a11ba4fe 100644 --- a/src/FloraWeb/Server/Auth/Types.hs +++ b/src/FloraWeb/Server/Auth/Types.hs @@ -4,7 +4,7 @@ import Data.Kind (Type) import Effectful import Effectful.Dispatch.Static import Effectful.Error.Static (Error) -import Effectful.Log (Logging) +import Effectful.Log (Log) import Effectful.PostgreSQL.Transact.Effect (DB) import Effectful.Reader.Static (Reader) import Effectful.Time (Time) @@ -72,7 +72,7 @@ type FloraPage = , DB , Time , Reader (Headers '[Header "Set-Cookie" SetCookie] Session) - , Logging + , Log , Error ServerError , IOE ] @@ -84,13 +84,13 @@ type FloraAdmin = , DB , Time , Reader (Headers '[Header "Set-Cookie" SetCookie] Session) - , Logging + , Log , Error ServerError , IOE ] -- | The effect stack for the development websockets -type FloraDevSocket = Eff [Reader (), Logging, Error ServerError, IOE] +type FloraDevSocket = Eff [Reader (), Log, Error ServerError, IOE] type instance AuthServerData (AuthProtect "optional-cookie-auth") = diff --git a/src/FloraWeb/Server/Logging.hs b/src/FloraWeb/Server/Logging.hs index b22b3381..6bd13711 100644 --- a/src/FloraWeb/Server/Logging.hs +++ b/src/FloraWeb/Server/Logging.hs @@ -1,3 +1,5 @@ +{-# OPTIONS_GHC -Wno-deferred-out-of-scope-variables #-} + module FloraWeb.Server.Logging ( makeLogger , runLog @@ -8,16 +10,16 @@ where import Data.Kind (Type) import Data.Text.Display (display) import Data.Time.Clock as Time (NominalDiffTime, diffUTCTime) +import Effectful +import Effectful.Log (Log) import Effectful.Log qualified as Log +import Effectful.Time (Time) import Effectful.Time qualified as Time -import Flora.Environment.Config -import Log (Logger, defaultLogLevel) +import Log (Logger) import Log.Backend.File (FileBackendConfig (..), withJSONFileBackend) +import Log.Backend.StandardOutput qualified as Log -import Effectful -import Effectful.Log (Logging) -import Effectful.Log.Backend.StandardOutput qualified as Log -import Effectful.Time +import Flora.Environment.Config -- | Wrapper around 'Log.runLogT' with necessary metadata runLog @@ -25,10 +27,10 @@ runLog . (IOE :> es) => DeploymentEnv -> Logger - -> Eff (Logging : es) a + -> Eff (Log : es) a -> Eff es a runLog env logger logAction = - Log.runLogging ("flora-" <> suffix) logger defaultLogLevel logAction + Log.runLog ("flora-" <> suffix) logger Log.defaultLogLevel logAction where suffix = display env diff --git a/src/FloraWeb/Types.hs b/src/FloraWeb/Types.hs index 7bbda893..013231b1 100644 --- a/src/FloraWeb/Types.hs +++ b/src/FloraWeb/Types.hs @@ -19,7 +19,7 @@ import Data.Kind (Type) import Data.Text.Encoding qualified as TE import Effectful import Effectful.Error.Static (Error) -import Effectful.Log (Logging) +import Effectful.Log (Log) import Effectful.Reader.Static (Reader) import Flora.Environment import GHC.Clock (getMonotonicTime) @@ -31,7 +31,7 @@ type Flora :: Type -> Type type Flora = Eff '[ Reader WebEnvStore - , Logging + , Log , Error ServerError , IOE ] diff --git a/src/Log/Backend/File.hs b/src/Log/Backend/File.hs index f98a28fa..96235aaf 100644 --- a/src/Log/Backend/File.hs +++ b/src/Log/Backend/File.hs @@ -5,9 +5,10 @@ import Data.ByteString.Char8 qualified as BS import Data.ByteString.Lazy qualified as BSL import Data.Kind (Type) import Effectful -import Effectful.Log.Logger qualified as Log +import Effectful.Log qualified as Log import GHC.Generics (Generic) import Log (Logger) +import Log.Internal.Logger (withLogger) import System.IO (stdout) data FileBackendConfig = FileBackendConfig @@ -21,8 +22,8 @@ withJSONFileBackend => FileBackendConfig -> (Logger -> Eff es a) -> Eff es a -withJSONFileBackend FileBackendConfig{destinationFile} action = do +withJSONFileBackend FileBackendConfig{destinationFile} action = withRunInIO $ \unlift -> do liftIO $ BS.hPutStrLn stdout $ BS.pack $ "Redirecting logs to " <> destinationFile - logger <- Log.mkLogger "file-json" $ \msg -> liftIO $ do + logger <- liftIO $ Log.mkLogger "file-json" $ \msg -> liftIO $ do BS.appendFile destinationFile (BSL.toStrict $ Aeson.encode msg <> "\n") - Log.withLogger logger action + withLogger logger (unlift . action) diff --git a/test/Flora/TestUtils.hs b/test/Flora/TestUtils.hs index 6f82827c..053ccf1c 100644 --- a/test/Flora/TestUtils.hs +++ b/test/Flora/TestUtils.hs @@ -67,8 +67,7 @@ import Database.PostgreSQL.Simple (Connection, SqlError (..), close) import Database.PostgreSQL.Simple.Migration import Database.PostgreSQL.Transact () import Effectful -import Effectful.Log -import Effectful.Log.Backend.StandardOutput qualified as Log +import Effectful.Log qualified as Log import Effectful.PostgreSQL.Transact.Effect import Effectful.Reader.Static import Effectful.Time @@ -90,6 +89,7 @@ import Test.Tasty (TestTree) import Test.Tasty qualified as Test import Test.Tasty.HUnit qualified as Test +import Effectful.Log (Log, Logger) import Flora.Environment import Flora.Environment.Config (LoggingDestination (..)) import Flora.Import.Categories (importCategories) @@ -101,8 +101,9 @@ import Flora.Model.User.Update qualified as Update import Flora.Publish import FloraWeb.Client import FloraWeb.Server.Logging qualified as Logging +import Log.Backend.StandardOutput qualified as Log -type TestEff = Eff '[DB, Logging, Time, IOE] +type TestEff = Eff '[DB, Log, Time, IOE] data Fixtures = Fixtures { hackageUser :: User @@ -122,12 +123,13 @@ importAllPackages fixtures = Log.withStdOutLogger $ \appLogger -> do "./test/fixtures/Cabal/" runTestEff :: TestEff a -> Pool Connection -> IO a -runTestEff comp pool = - runEff - . runCurrentTimeIO - . Log.runSimpleStdOutLogging "flora-test" LogAttention - . runDB pool - $ comp +runTestEff comp pool = runEff $ + Log.withStdOutLogger $ \stdOutLogger -> + do + runCurrentTimeIO + . Log.runLog "flora-test" stdOutLogger LogAttention + . runDB pool + $ comp testThis :: String -> TestEff () -> TestEff TestTree testThis name assertion = do diff --git a/test/Main.hs b/test/Main.hs index 687d0866..287cee76 100644 --- a/test/Main.hs +++ b/test/Main.hs @@ -1,14 +1,15 @@ module Main where import Effectful -import Effectful.Log.Backend.StandardOutput qualified as Log import Effectful.PostgreSQL.Transact.Effect import Effectful.Time +import Log.Backend.StandardOutput qualified as Log import Log.Data import Optics.Core import System.IO import Test.Tasty (defaultMain, testGroup) +import Effectful.Log qualified as Log import Flora.CabalSpec qualified as CabalSpec import Flora.CategorySpec qualified as CategorySpec import Flora.Environment @@ -22,9 +23,9 @@ main :: IO () main = do hSetBuffering stdout LineBuffering env <- runEff getFloraTestEnv - fixtures <- runEff - . runCurrentTimeIO - . Log.runSimpleStdOutLogging "flora-test" LogAttention + fixtures <- runEff $ Log.withStdOutLogger $ \stdOutLogger -> do + runCurrentTimeIO + . Log.runLog "flora-test" stdOutLogger LogAttention . runDB (env ^. #pool) $ do testMigrations From 967b0c622add2d47919d8a2558fe47fdcb7d9f05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Mon, 10 Oct 2022 21:35:38 +0200 Subject: [PATCH 11/29] Update the README for the new Nix build system --- README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 14554eff..2634ac88 100644 --- a/README.md +++ b/README.md @@ -38,11 +38,10 @@ ## Installation and Configuration -For ease of development, a `shell.nix` file is provided. It brings with it system dependency and tooling. -To jump into the development environment, use `make nix-shell`. It is impure by default, so your editor and development -tools will still be accessible. +Read the Contribution Guide file to see what you will need. + +For Nix users, a build system based on flakes is maintained on a “best-effort” basis. Improvements are very welcome. -Otherwise, read the Contribution Guide file to see what you will need. ### Flora server @@ -95,7 +94,7 @@ $ make docker-start $ make docker-enter # You'll be in the docker container. Environment variables are automatically set # so you should be able to start Flora -(docker)$ make nix-tmux +(docker)$ make start-tmux # You'll be in a tmux session, everything should be launched # Visit localhost:8084 from your web browser to see if it all works. @@ -104,7 +103,7 @@ $ make docker-enter (docker)$ source environment.docker.sh (docker)$ make db-drop # password is 'postgres' by default (docker)$ make db-setup # password is 'postgres' by default -(docker)$ make nix-provision +(docker)$ make db-provision # And you should be good! ``` From 08091dd14367da9b482c713fbf92fb45e24f5191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Wed, 12 Oct 2022 00:39:19 +0200 Subject: [PATCH 12/29] [FLORA-147] Support release changelogs (#241) * [FLORA-147] Support Changelogs closes #147 * Add changelog entry --- CHANGELOG.md | 1 + assets/css/app.css | 5 +- assets/css/release-changelog.css | 108 ++++++++++++++++++ ...{package-readme.css => release-readme.css} | 4 +- assets/css/variables.css | 4 + flora.cabal | 8 +- migrations/20221011215519_add_changelogs.sql | 22 ++++ src/Flora/Import/Package.hs | 2 + src/Flora/Model/Release/Query.hs | 16 +++ src/Flora/Model/Release/Types.hs | 30 ++--- src/Flora/Model/Release/Update.hs | 15 ++- src/Flora/OddJobs.hs | 80 +++++++------ src/Flora/OddJobs/Render.hs | 37 ++++++ src/Flora/OddJobs/Types.hs | 17 ++- src/Flora/ThirdParties/Hackage/API.hs | 1 + src/Flora/ThirdParties/Hackage/Client.hs | 7 ++ src/FloraWeb/Links.hs | 17 +++ src/FloraWeb/Routes/Pages/Admin.hs | 18 +-- src/FloraWeb/Routes/Pages/Packages.hs | 13 +++ src/FloraWeb/Server.hs | 4 +- src/FloraWeb/Server/Pages/Admin.hs | 30 ++--- src/FloraWeb/Server/Pages/Packages.hs | 28 ++++- src/FloraWeb/Templates/Admin.hs | 15 +-- src/FloraWeb/Templates/Packages/Changelog.hs | 24 ++++ src/FloraWeb/Templates/Pages/Packages.hs | 17 ++- test/Flora/OddJobSpec.hs | 21 +--- 26 files changed, 410 insertions(+), 134 deletions(-) create mode 100644 assets/css/release-changelog.css rename assets/css/{package-readme.css => release-readme.css} (97%) create mode 100644 migrations/20221011215519_add_changelogs.sql create mode 100644 src/Flora/OddJobs/Render.hs create mode 100644 src/FloraWeb/Templates/Packages/Changelog.hs diff --git a/CHANGELOG.md b/CHANGELOG.md index 812ccc23..52c345d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * Reorder the package page columns in mobile view (#233) * Enable the use of markdown extensions in package READMEs (#236) * Autofocus the search field on the home page (#235) +* Support release changelogs (#241) ## v1.0.4 -- 2022-10-02 diff --git a/assets/css/app.css b/assets/css/app.css index bdb10c10..3fb65bbe 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -5,7 +5,8 @@ @import "tailwindcss/utilities"; @import "variables.css"; -@import "package-readme.css"; +@import "release-readme.css"; +@import "release-changelog.css"; @layer components { .larger-container { @@ -439,7 +440,7 @@ div[class="bullets"] { order: 2; } - .package-readme-column { + .release-readme-column { order: 3; } } diff --git a/assets/css/release-changelog.css b/assets/css/release-changelog.css new file mode 100644 index 00000000..547cdc68 --- /dev/null +++ b/assets/css/release-changelog.css @@ -0,0 +1,108 @@ +.release-changelog { + overflow-wrap: break-word; + box-sizing: border-box; + + h1 { + border-bottom: 1px solid #354561; + padding-bottom: 0.3em; + margin-bottom: 16px; + font-size: 2em; + + a { + display: inline-block; + } + } + + img { + max-width: 100%; + box-sizing: content-box; + display: inline-block; + } + + h2 { + border-bottom: 1px solid #354561; + margin-bottom: 16px; + margin-top: 16px; + padding-bottom: 0.3em; + font-size: 1.5em; + } + + h3 { + border-bottom: 1px solid #354561; + margin-bottom: 16px; + margin-top: 16px; + padding-bottom: 0.3em; + font-size: 1.25em; + } + + h4 { + border-bottom: 1px solid #354561; + margin-bottom: 16px; + margin-top: 16px; + padding-bottom: 0.3em; + font-size: 1em; + } + + p { + overflow-wrap: break-word; + margin-bottom: 16px; + box-sizing: border-box; + + a { + display: inline-block; + } + } + + ol { + margin-bottom: 16px; + padding-left: 1em; + + li { + overflow-wrap: break-word; + list-style-type: decimal; + list-style-position: outside; + + p { + display: inline; + } + } + } + + ul { + margin-bottom: 16px; + padding-left: 1em; + + li { + overflow-wrap: break-word; + list-style-type: disc; + list-style-position: outside; + margin-bottom: 10px; + + p { + display: inline; + } + } + + li + li { + margin-top: 0.25em; + } + } + + pre { + line-height: 1.45; + padding: 16px; + font-family: monospace; + border-radius: 6px; + overflow: auto; + font-size: 90%; + background-color: var(--changelog-pre-background-color); + margin-bottom: 16px; + } + + code { + margin: 0; + font-size: 85%; + background-color: var(--changelog-code-background-color); + border-radius: 6px; + } +} diff --git a/assets/css/package-readme.css b/assets/css/release-readme.css similarity index 97% rename from assets/css/package-readme.css rename to assets/css/release-readme.css index c2c3f4ae..e1b504cc 100644 --- a/assets/css/package-readme.css +++ b/assets/css/release-readme.css @@ -1,9 +1,9 @@ -.package-readme-column { +.release-readme-column { margin: 0 2em; overflow: auto; } -.package-readme { +.release-readme { overflow-wrap: break-word; box-sizing: border-box; diff --git a/assets/css/variables.css b/assets/css/variables.css index 6fa3bb28..7e25bd15 100644 --- a/assets/css/variables.css +++ b/assets/css/variables.css @@ -32,6 +32,8 @@ --search-bar-focus-border-color: hsl(294 40% 30%); --readme-pre-background-color: hsl(225 13% 87%); --readme-code-background-color: hsl(225 13% 87%); + --changelog-pre-background-color: hsl(225 13% 87%); + --changelog-code-background-color: hsl(225 13% 87%); --install-string-border: hsl(215 28% 17%); } @@ -66,5 +68,7 @@ html[data-theme="dark"] { --search-bar-focus-border-color: hsl(294 40% 30%); --readme-pre-background-color: hsl(218 29% 30%); --readme-code-background-color: hsl(218 29% 30%); + --changelog-pre-background-color: hsl(218 29% 30%); + --changelog-code-background-color: hsl(218 29% 30%); --install-string-border: hsl(215 28% 17%); } diff --git a/flora.cabal b/flora.cabal index 0b0abc75..22fceb48 100644 --- a/flora.cabal +++ b/flora.cabal @@ -33,7 +33,6 @@ common common-extensions DuplicateRecordFields LambdaCase OverloadedLabels - OverloadedRecordDot OverloadedStrings PolyKinds QuasiQuotes @@ -42,6 +41,7 @@ common common-extensions TypeFamilies UndecidableInstances ViewPatterns + OverloadedRecordDot default-language: GHC2021 @@ -108,6 +108,7 @@ library Flora.Model.User.Query Flora.Model.User.Update Flora.OddJobs + Flora.OddJobs.Render Flora.OddJobs.Types Flora.Publish Flora.Search @@ -154,6 +155,7 @@ library FloraWeb.Templates.Admin.Packages FloraWeb.Templates.Admin.Users FloraWeb.Templates.Error + FloraWeb.Templates.Packages.Changelog FloraWeb.Templates.Packages.Dependencies FloraWeb.Templates.Packages.Dependents FloraWeb.Templates.Packages.Listing @@ -193,13 +195,13 @@ library , envparse ^>=0.5 , filepath ^>=1.4 , http-api-data ^>=0.4 - , http-client >=0.7.10 && <0.8 + , http-client ^>=0.7.10 , http-client-tls , http-media , http-types ^>=0.12 , iso8601-time ^>=0.1 , lens - , log-base >=0.12 && <0.13 + , log-base ^>=0.12 , log-effectful ^>=1.0 , lucid ^>=2.11 , lucid-alpine ^>=0.1 diff --git a/migrations/20221011215519_add_changelogs.sql b/migrations/20221011215519_add_changelogs.sql new file mode 100644 index 00000000..dce8b787 --- /dev/null +++ b/migrations/20221011215519_add_changelogs.sql @@ -0,0 +1,22 @@ +alter type readme_status + rename to import_status; + +alter table releases + add column changelog text, + add column changelog_status import_status; + +alter table releases + add constraint consistent_readme_status + check ( + ((readme_status = 'imported' or readme_status = 'inexistent') + and readme is not null) + or (readme_status = 'not-imported' and readme is null) + ), + add constraint consistent_changelog_status + check ( + ((changelog_status = 'imported' or changelog_status = 'inexistent') + and changelog is not null) + or (changelog_status = 'not-imported' and changelog is null) + ); + +create index on releases(changelog_status); diff --git a/src/Flora/Import/Package.hs b/src/Flora/Import/Package.hs index aed5990e..26c9d1b8 100644 --- a/src/Flora/Import/Package.hs +++ b/src/Flora/Import/Package.hs @@ -258,6 +258,8 @@ extractPackageDataFromCabal userId genericDesc = do , updatedAt = timestamp , readme = Nothing , readmeStatus = NotImported + , changelog = Nothing + , changelogStatus = NotImported } let lib = extractLibrary package release Nothing Nothing <$> allLibraries packageDesc diff --git a/src/Flora/Model/Release/Query.hs b/src/Flora/Model/Release/Query.hs index 05ca6a2c..d1d83fe2 100644 --- a/src/Flora/Model/Release/Query.hs +++ b/src/Flora/Model/Release/Query.hs @@ -6,6 +6,7 @@ module Flora.Model.Release.Query , getReleaseByVersion , getPackageReleases , getPackageReleasesWithoutReadme + , getPackageReleasesWithoutChangelog , getPackageReleasesWithoutUploadTimestamp , getAllReleases , getNumberOfReleases @@ -89,6 +90,21 @@ getPackageReleasesWithoutUploadTimestamp = where r.uploaded_at is null |] +getPackageReleasesWithoutChangelog :: [DB, IOE] :>> es => Eff es (Vector (ReleaseId, Version, PackageName)) +getPackageReleasesWithoutChangelog = + dbtToEff $ + query Select querySpec () + where + querySpec :: Query + querySpec = + [sql| + select r.release_id, r.version, p."name" + from releases as r + join packages as p + on p.package_id = r.package_id + where r.changelog_status = 'not-imported' + |] + getReleaseByVersion :: [DB, IOE] :>> es => PackageId -> Version -> Eff es (Maybe Release) getReleaseByVersion packageId version = dbtToEff $ queryOne Select (_selectWhere @Release [[field| package_id |], [field| version |]]) (packageId, version) diff --git a/src/Flora/Model/Release/Types.hs b/src/Flora/Model/Release/Types.hs index 51062efe..d48e3bb8 100644 --- a/src/Flora/Model/Release/Types.hs +++ b/src/Flora/Model/Release/Types.hs @@ -3,7 +3,7 @@ module Flora.Model.Release.Types , TextHtml (..) , Release (..) , ReleaseMetadata (..) - , ReadmeStatus (..) + , ImportStatus (..) ) where @@ -72,7 +72,11 @@ data Release = Release -- ^ Last update timestamp for this release , readme :: Maybe TextHtml -- ^ Content of the release's README - , readmeStatus :: ReadmeStatus + , readmeStatus :: ImportStatus + -- ^ Import status of the README + , changelog :: Maybe TextHtml + -- ^ Content of the release's Changelog + , changelogStatus :: ImportStatus } deriving stock (Eq, Show, Generic) deriving anyclass (FromRow, ToRow) @@ -83,29 +87,29 @@ data Release = Release instance Ord Release where compare x y = compare (x.version) (y.version) -data ReadmeStatus +data ImportStatus = Imported | Inexistent | NotImported deriving stock (Eq, Ord, Show, Enum, Bounded, Generic) -parseReadmeStatus :: ByteString -> Maybe ReadmeStatus -parseReadmeStatus "imported" = pure Imported -parseReadmeStatus "inexistent" = pure Inexistent -parseReadmeStatus "not-imported" = pure NotImported -parseReadmeStatus _ = Nothing +parseImportStatus :: ByteString -> Maybe ImportStatus +parseImportStatus "imported" = pure Imported +parseImportStatus "inexistent" = pure Inexistent +parseImportStatus "not-imported" = pure NotImported +parseImportStatus _ = Nothing -instance Display ReadmeStatus where +instance Display ImportStatus where displayBuilder Imported = "imported" displayBuilder Inexistent = "inexistent" displayBuilder NotImported = "not-imported" -instance FromField ReadmeStatus where +instance FromField ImportStatus where fromField f Nothing = returnError UnexpectedNull f "" - fromField _ (Just bs) | Just status <- parseReadmeStatus bs = pure status - fromField f (Just bs) = returnError ConversionFailed f $ unpack $ "Conversion error: Expected component to be one of " <> display @[ReadmeStatus] [minBound .. maxBound] <> ", but instead got " <> decodeUtf8 bs + fromField _ (Just bs) | Just status <- parseImportStatus bs = pure status + fromField f (Just bs) = returnError ConversionFailed f $ unpack $ "Conversion error: Expected component to be one of " <> display @[ImportStatus] [minBound .. maxBound] <> ", but instead got " <> decodeUtf8 bs -instance ToField ReadmeStatus where +instance ToField ImportStatus where toField = Escape . encodeUtf8 . display data ReleaseMetadata = ReleaseMetadata diff --git a/src/Flora/Model/Release/Update.hs b/src/Flora/Model/Release/Update.hs index d83438c0..30b9a2f1 100644 --- a/src/Flora/Model/Release/Update.hs +++ b/src/Flora/Model/Release/Update.hs @@ -13,7 +13,7 @@ import Effectful import Effectful.PostgreSQL.Transact.Effect import Data.Time (UTCTime) -import Flora.Model.Release.Types (ReadmeStatus (..), Release, ReleaseId, TextHtml (..)) +import Flora.Model.Release.Types (ImportStatus (..), Release, ReleaseId, TextHtml (..)) insertRelease :: ([DB, IOE] :>> es) => Release -> Eff es () insertRelease = dbtToEff . insert @Release @@ -24,7 +24,7 @@ upsertRelease release = dbtToEff $ upsert @Release release [[field| updated_at | refreshLatestVersions :: ([DB, IOE] :>> es) => Eff es () refreshLatestVersions = dbtToEff $ void $ execute Update [sql| REFRESH MATERIALIZED VIEW CONCURRENTLY "latest_versions" |] () -updateReadme :: ([DB, IOE] :>> es) => ReleaseId -> Maybe TextHtml -> ReadmeStatus -> Eff es () +updateReadme :: ([DB, IOE] :>> es) => ReleaseId -> Maybe TextHtml -> ImportStatus -> Eff es () updateReadme releaseId readmeBody status = dbtToEff $ void $ @@ -43,3 +43,14 @@ updateUploadTime releaseId timestamp = [[field| uploaded_at |]] ([field| release_id |], releaseId) (Only (Just timestamp)) + +updateChangelog :: ([DB, IOE] :>> es) => ReleaseId -> Maybe TextHtml -> ImportStatus -> Eff es () +updateChangelog releaseId changelogBody status = + dbtToEff $ + void $ + updateFieldsBy @Release + [ [field| changelog |] + , [field| changelog_status |] + ] + ([field| release_id |], releaseId) + (changelogBody, status) diff --git a/src/Flora/OddJobs.hs b/src/Flora/OddJobs.hs index b0c90b85..4ddd60d5 100644 --- a/src/Flora/OddJobs.hs +++ b/src/Flora/OddJobs.hs @@ -3,6 +3,7 @@ -- | Represents the various jobs that can be run module Flora.OddJobs ( scheduleReadmeJob + , scheduleChangelogJob , scheduleUploadTimeJob , scheduleIndexImportJob , checkIfIndexImportJobIsNotRunning @@ -12,23 +13,19 @@ module Flora.OddJobs -- * exposed for testing -- prefer using smart constructors. - , ReadmePayload (..) + , ReadmeJobPayload (..) , FloraOddJobs (..) , IntAesonVersion (..) ) where -import Commonmark qualified -import Commonmark.Extensions qualified import Control.Concurrent (forkIO) import Control.Exception import Control.Monad import Control.Monad.IO.Class import Data.Aeson (Result (..), fromJSON, toJSON) import Data.Pool -import Data.Text import Data.Text.Display -import Data.Text.Lazy qualified as TL import Data.Text.Lazy.Encoding qualified as TL import Data.Time qualified as Time import Database.PostgreSQL.Entity.DBT @@ -38,7 +35,6 @@ import Database.PostgreSQL.Simple.SqlQQ (sql) import Distribution.Types.Version import Effectful.PostgreSQL.Transact.Effect import Log -import Lucid qualified import Network.HTTP.Types (gone410, notFound404, statusCode) import OddJobs.Job (Job (..), createJob, scheduleJob) import Servant.Client (ClientError (..)) @@ -49,6 +45,7 @@ import Flora.Model.Package import Flora.Model.Release.Query qualified as Query import Flora.Model.Release.Types import Flora.Model.Release.Update qualified as Update +import Flora.OddJobs.Render (renderMarkdown) import Flora.OddJobs.Types import Flora.ThirdParties.Hackage.API (VersionedPackage (..)) import Flora.ThirdParties.Hackage.Client qualified as Hackage @@ -59,7 +56,15 @@ scheduleReadmeJob pool rid package version = createJob res jobTableName - (MkReadme $ MkReadmePayload package rid $ MkIntAesonVersion version) + (FetchReadme $ ReadmeJobPayload package rid $ MkIntAesonVersion version) + +scheduleChangelogJob :: Pool PG.Connection -> ReleaseId -> PackageName -> Version -> IO Job +scheduleChangelogJob pool rid package version = + withResource pool $ \res -> + createJob + res + jobTableName + (FetchChangelog $ ChangelogJobPayload package rid $ MkIntAesonVersion version) scheduleUploadTimeJob :: Pool PG.Connection -> ReleaseId -> PackageName -> Version -> IO Job scheduleUploadTimeJob pool releaseId packageName version = do @@ -67,7 +72,7 @@ scheduleUploadTimeJob pool releaseId packageName version = do createJob res jobTableName - (FetchUploadTime $ FetchUploadTimePayload packageName releaseId (MkIntAesonVersion version)) + (FetchUploadTime $ UploadTimeJobPayload packageName releaseId (MkIntAesonVersion version)) scheduleIndexImportJob :: Pool PG.Connection -> IO Job scheduleIndexImportJob pool = do @@ -106,50 +111,42 @@ checkIfIndexImportJobIsNotRunning = do Log.logInfo_ "Index import job not running" pure False -makeReadme :: ReadmePayload -> JobsRunner () -makeReadme pay@MkReadmePayload{..} = localDomain "fetch-readme" $ do +fetchChangeLog :: ChangelogJobPayload -> JobsRunner () +fetchChangeLog payload@ChangelogJobPayload{packageName, packageVersion, releaseId} = localDomain "fetch-changelog" $ do + Log.logInfo "Fetching CHANGELOG" payload + let requestPayload = VersionedPackage packageName packageVersion + result <- Hackage.request $ Hackage.getPackageChangelog requestPayload + case result of + Left e@(FailureResponse _ response) + -- If the CHANGELOG simply doesn't exist, we skip it by marking the job as successful. + | response.responseStatusCode == notFound404 -> Update.updateChangelog releaseId Nothing Inexistent + | response.responseStatusCode == gone410 -> Update.updateChangelog releaseId Nothing Inexistent + | otherwise -> throw e + Left e -> throw e + Right bodyText -> do + logInfo ("got a changelog for package " <> display packageName) (object ["release_id" .= releaseId]) + let readmeBody = renderMarkdown ("CHANGELOG" <> show packageName) bodyText + Update.updateChangelog releaseId (Just $ MkTextHtml readmeBody) Imported + +makeReadme :: ReadmeJobPayload -> JobsRunner () +makeReadme pay@ReadmeJobPayload{..} = localDomain "fetch-readme" $ do logInfo "Fetching README" pay let payload = VersionedPackage mpPackage mpVersion gewt <- Hackage.request $ Hackage.getPackageReadme payload case gewt of Left e@(FailureResponse _ response) - -- If the README simply doesn't exist, we skip it by marking it as successful. + -- If the README simply doesn't exist, we skip it by marking the job as successful. | response.responseStatusCode == notFound404 -> Update.updateReadme mpReleaseId Nothing Inexistent | response.responseStatusCode == gone410 -> Update.updateReadme mpReleaseId Nothing Inexistent | otherwise -> throw e Left e -> throw e Right bodyText -> do - logInfo ("got a body for package " <> display mpPackage) (object ["release_id" .= mpReleaseId]) - - htmlTxt <- do - let extensions = - mconcat - [ Commonmark.Extensions.mathSpec - , -- all gfm extensions apart from pipeTable - Commonmark.Extensions.emojiSpec - , Commonmark.Extensions.strikethroughSpec - , Commonmark.Extensions.autolinkSpec - , Commonmark.Extensions.autoIdentifiersSpec - , Commonmark.Extensions.taskListSpec - , Commonmark.Extensions.footnoteSpec - , -- default syntax - Commonmark.defaultSyntaxSpec - , -- pipe table spec. This has to be after default syntax due to - -- https://github.com/jgm/commonmark-hs/issues/95 - Commonmark.Extensions.pipeTableSpec - ] - Commonmark.commonmarkWith extensions ("readme " <> show mpPackage) bodyText - >>= \case - Left exception -> throw (MarkdownFailed exception) - Right (y :: Commonmark.Html ()) -> pure $ Commonmark.renderHtml y - - let readmeBody :: Lucid.Html () - readmeBody = Lucid.toHtmlRaw @Text $ TL.toStrict htmlTxt - + logInfo ("got a readme for package " <> display mpPackage) (object ["release_id" .= mpReleaseId]) + let readmeBody = renderMarkdown ("README" <> show mpPackage) bodyText Update.updateReadme mpReleaseId (Just $ MkTextHtml readmeBody) Imported -fetchUploadTime :: FetchUploadTimePayload -> JobsRunner () -fetchUploadTime payload@FetchUploadTimePayload{packageName, packageVersion, releaseId} = localDomain "fetch-upload-time" $ do +fetchUploadTime :: UploadTimeJobPayload -> JobsRunner () +fetchUploadTime payload@UploadTimeJobPayload{packageName, packageVersion, releaseId} = localDomain "fetch-upload-time" $ do logInfo "Fetching upload time" payload let requestPayload = VersionedPackage packageName packageVersion result <- Hackage.request $ Hackage.getPackageUploadTime requestPayload @@ -185,6 +182,7 @@ runner job = localDomain "job-runner" $ case fromJSON (jobPayload job) of Error str -> logMessage LogAttention "decode error" (toJSON str) Success val -> case val of - MkReadme x -> makeReadme x + FetchReadme x -> makeReadme x FetchUploadTime x -> fetchUploadTime x + FetchChangelog x -> fetchChangeLog x ImportHackageIndex _ -> fetchNewIndex diff --git a/src/Flora/OddJobs/Render.hs b/src/Flora/OddJobs/Render.hs new file mode 100644 index 00000000..4954f5c3 --- /dev/null +++ b/src/Flora/OddJobs/Render.hs @@ -0,0 +1,37 @@ +module Flora.OddJobs.Render where + +import Commonmark qualified +import Commonmark.Extensions qualified as Commonmark +import Control.Exception +import Data.Text (Text) +import Data.Text.Lazy qualified as TL +import Flora.OddJobs.Types (OddJobException (..)) +import Lucid (Html, toHtmlRaw) + +renderMarkdown :: String -> Text -> Html () +renderMarkdown name bodyText = do + htmlTxt <- do + let extensions = + mconcat + [ Commonmark.mathSpec + , -- all gfm extensions apart from pipeTable + Commonmark.emojiSpec + , Commonmark.strikethroughSpec + , Commonmark.autolinkSpec + , Commonmark.autoIdentifiersSpec + , Commonmark.taskListSpec + , Commonmark.footnoteSpec + , -- default syntax + Commonmark.defaultSyntaxSpec + , Commonmark.autoIdentifiersSpec + , Commonmark.implicitHeadingReferencesSpec + , -- pipe table spec. This has to be after default syntax due to + -- https://github.com/jgm/commonmark-hs/issues/95 + Commonmark.pipeTableSpec + ] + Commonmark.commonmarkWith extensions name bodyText + >>= \case + Left exception -> throw (MarkdownFailed exception) + Right (y :: Commonmark.Html ()) -> pure $ Commonmark.renderHtml y + + toHtmlRaw @Text $ TL.toStrict htmlTxt diff --git a/src/Flora/OddJobs/Types.hs b/src/Flora/OddJobs/Types.hs index 1304bd0b..0944b046 100644 --- a/src/Flora/OddJobs/Types.hs +++ b/src/Flora/OddJobs/Types.hs @@ -87,7 +87,7 @@ instance ToJSON IntAesonVersion where instance FromJSON IntAesonVersion where parseJSON val = MkIntAesonVersion . mkVersion <$> parseJSON val -data ReadmePayload = MkReadmePayload +data ReadmeJobPayload = ReadmeJobPayload { mpPackage :: PackageName , mpReleaseId :: ReleaseId -- needed to write the readme in db , mpVersion :: IntAesonVersion @@ -95,7 +95,15 @@ data ReadmePayload = MkReadmePayload deriving stock (Generic) deriving anyclass (ToJSON, FromJSON) -data FetchUploadTimePayload = FetchUploadTimePayload +data UploadTimeJobPayload = UploadTimeJobPayload + { packageName :: PackageName + , releaseId :: ReleaseId + , packageVersion :: IntAesonVersion + } + deriving stock (Generic) + deriving anyclass (ToJSON, FromJSON) + +data ChangelogJobPayload = ChangelogJobPayload { packageName :: PackageName , releaseId :: ReleaseId , packageVersion :: IntAesonVersion @@ -109,8 +117,9 @@ data ImportHackageIndexPayload = ImportHackageIndexPayload -- these represent the possible odd jobs we can run. data FloraOddJobs - = MkReadme ReadmePayload - | FetchUploadTime FetchUploadTimePayload + = FetchReadme ReadmeJobPayload + | FetchUploadTime UploadTimeJobPayload + | FetchChangelog ChangelogJobPayload | ImportHackageIndex ImportHackageIndexPayload deriving stock (Generic) deriving anyclass (ToJSON, FromJSON) diff --git a/src/Flora/ThirdParties/Hackage/API.hs b/src/Flora/ThirdParties/Hackage/API.hs index 9b7ed95d..f6d88871 100644 --- a/src/Flora/ThirdParties/Hackage/API.hs +++ b/src/Flora/ThirdParties/Hackage/API.hs @@ -48,6 +48,7 @@ data HackageAPI' mode = HackageAPI' data HackagePackageAPI mode = HackagePackageAPI { getReadme :: mode :- "readme.txt" :> Get '[PlainerText] Text , getUploadTime :: mode :- "upload-time" :> Get '[PlainText] UTCTime + , getChangelog :: mode :- "changelog.txt" :> Get '[PlainerText] Text } deriving stock (Generic) diff --git a/src/Flora/ThirdParties/Hackage/Client.hs b/src/Flora/ThirdParties/Hackage/Client.hs index 36255e58..f6c3c2c6 100644 --- a/src/Flora/ThirdParties/Hackage/Client.hs +++ b/src/Flora/ThirdParties/Hackage/Client.hs @@ -46,3 +46,10 @@ getPackageUploadTime packageName = // API.withPackage /: packageName // API.getUploadTime + +getPackageChangelog :: VersionedPackage -> ClientM Text +getPackageChangelog versionedPackage = + hackageClient + // API.withPackage + /: versionedPackage + // API.getChangelog diff --git a/src/FloraWeb/Links.hs b/src/FloraWeb/Links.hs index fec6f7b1..b9339853 100644 --- a/src/FloraWeb/Links.hs +++ b/src/FloraWeb/Links.hs @@ -32,6 +32,23 @@ packageVersionLink namespace packageName version = /: packageName /: version +packageVersionChangelog :: Namespace -> PackageName -> Version -> Link +packageVersionChangelog namespace packageName version = + links + // Web.packages + // Web.showVersionChangelog + /: namespace + /: packageName + /: version + +packageChangelog :: Namespace -> PackageName -> Link +packageChangelog namespace packageName = + links + // Web.packages + // Web.showChangelog + /: namespace + /: packageName + packageIndexLink :: Word -> Link packageIndexLink pageNumber = links diff --git a/src/FloraWeb/Routes/Pages/Admin.hs b/src/FloraWeb/Routes/Pages/Admin.hs index 9315c50f..385d4c67 100644 --- a/src/FloraWeb/Routes/Pages/Admin.hs +++ b/src/FloraWeb/Routes/Pages/Admin.hs @@ -10,18 +10,11 @@ import Servant.HTML.Lucid type Routes = NamedRoutes Routes' -type MakeReadmes = - "readmes" - :> Verb 'POST 301 '[HTML] MakeReadmesResponse +type FetchMetadata = + "metadata" + :> Verb 'POST 301 '[HTML] FetchMetadataResponse -type MakeReadmesResponse = - Headers '[Header "Location" Text] NoContent - -type FetchUploadTimes = - "upload-times" - :> Verb 'POST 301 '[HTML] FetchUploadTimesResponse - -type FetchUploadTimesResponse = +type FetchMetadataResponse = Headers '[Header "Location" Text] NoContent type ImportIndex = @@ -33,8 +26,7 @@ type ImportIndexResponse = data Routes' mode = Routes' { index :: mode :- Get '[HTML] (Html ()) - , makeReadmes :: mode :- MakeReadmes - , fetchUploadTimes :: mode :- FetchUploadTimes + , fetchMetadata :: mode :- FetchMetadata , importIndex :: mode :- ImportIndex , oddJobs :: mode :- "odd-jobs" :> OddJobs.FinalAPI -- they compose :o , users :: mode :- "users" :> AdminUsersRoutes diff --git a/src/FloraWeb/Routes/Pages/Packages.hs b/src/FloraWeb/Routes/Pages/Packages.hs index 9b56e09d..8230e391 100644 --- a/src/FloraWeb/Routes/Pages/Packages.hs +++ b/src/FloraWeb/Routes/Pages/Packages.hs @@ -35,6 +35,19 @@ data Routes' mode = Routes' :> Capture "package" PackageName :> "dependencies" :> Get '[HTML] (Html ()) + , showChangelog + :: mode + :- Capture "namespace" Namespace + :> Capture "package" PackageName + :> "changelog" + :> Get '[HTML] (Html ()) + , showVersionChangelog + :: mode + :- Capture "namespace" Namespace + :> Capture "package" PackageName + :> Capture "version" Version + :> "changelog" + :> Get '[HTML] (Html ()) , showVersion :: mode :- Capture "namespace" Namespace diff --git a/src/FloraWeb/Server.hs b/src/FloraWeb/Server.hs index 51c3fde7..d3af7548 100644 --- a/src/FloraWeb/Server.hs +++ b/src/FloraWeb/Server.hs @@ -42,7 +42,7 @@ import Control.Exception.Safe qualified as Safe import Effectful.Concurrent import Effectful.Reader.Static (runReader, withReader) import Effectful.Servant (effToHandler) -import Effectful.Time (Time, runCurrentTimeIO) +import Effectful.Time (runCurrentTimeIO) import Network.HTTP.Client qualified as HTTP import OddJobs.Endpoints qualified as OddJobs import OddJobs.Job (startJobRunner) @@ -104,7 +104,7 @@ logException env logger exception = . Logging.runLog env logger $ Log.logAttention "odd-jobs runner crashed " (show exception) -runServer :: (Time :> es, Concurrent :> es, IOE :> es) => Logger -> FloraEnv -> Eff es () +runServer :: (Concurrent :> es, IOE :> es) => Logger -> FloraEnv -> Eff es () runServer appLogger floraEnv = do httpManager <- liftIO $ HTTP.newManager tlsManagerSettings let runnerEnv = JobsRunnerEnv httpManager diff --git a/src/FloraWeb/Server/Pages/Admin.hs b/src/FloraWeb/Server/Pages/Admin.hs index f7eed554..7291cf25 100644 --- a/src/FloraWeb/Server/Pages/Admin.hs +++ b/src/FloraWeb/Server/Pages/Admin.hs @@ -40,8 +40,7 @@ server cfg env = , users = adminUsersHandler , packages = adminPackagesHandler , oddJobs = OddJobs.server cfg env handlerToEff - , makeReadmes = makeReadmesHandler - , fetchUploadTimes = fetchUploadTimesHandler + , fetchMetadata = fetchMetadataHandler , importIndex = indexImportJobHandler } @@ -72,25 +71,26 @@ indexHandler = do report <- liftIO $ withPool pool getReport render templateEnv (Templates.index report) -makeReadmesHandler :: FloraAdmin MakeReadmesResponse -makeReadmesHandler = do +fetchMetadataHandler :: FloraAdmin FetchMetadataResponse +fetchMetadataHandler = do session <- getSession FloraEnv{jobsPool} <- liftIO $ fetchFloraEnv (session.webEnvStore) - releases <- Query.getPackageReleasesWithoutReadme - liftIO $ forkIO $ forM_ releases $ \(releaseId, version, packagename) -> do - scheduleReadmeJob jobsPool releaseId packagename version - pure $ redirect "/admin" -fetchUploadTimesHandler :: FloraAdmin FetchUploadTimesResponse -fetchUploadTimesHandler = do - session <- getSession - FloraEnv{jobsPool} <- liftIO $ fetchFloraEnv (session.webEnvStore) - releases <- Query.getPackageReleasesWithoutUploadTimestamp - liftIO $ forkIO $ forM_ releases $ \(releaseId, version, packagename) -> do + releasesWithoutReadme <- Query.getPackageReleasesWithoutReadme + liftIO $ forkIO $ forM_ releasesWithoutReadme $ \(releaseId, version, packagename) -> do + Async.async $ scheduleReadmeJob jobsPool releaseId packagename version + + releasesWithoutUploadTime <- Query.getPackageReleasesWithoutUploadTimestamp + liftIO $ forkIO $ forM_ releasesWithoutUploadTime $ \(releaseId, version, packagename) -> do Async.async $ scheduleUploadTimeJob jobsPool releaseId packagename version + + releasesWithoutChangelog <- Query.getPackageReleasesWithoutChangelog + liftIO $ forkIO $ forM_ releasesWithoutChangelog $ \(releaseId, version, packagename) -> do + Async.async $ scheduleChangelogJob jobsPool releaseId packagename version + pure $ redirect "/admin" -indexImportJobHandler :: FloraAdmin FetchUploadTimesResponse +indexImportJobHandler :: FloraAdmin ImportIndexResponse indexImportJobHandler = do session <- getSession FloraEnv{jobsPool} <- liftIO $ fetchFloraEnv (session.webEnvStore) diff --git a/src/FloraWeb/Server/Pages/Packages.hs b/src/FloraWeb/Server/Pages/Packages.hs index 45aacbd0..456b6668 100644 --- a/src/FloraWeb/Server/Pages/Packages.hs +++ b/src/FloraWeb/Server/Pages/Packages.hs @@ -14,6 +14,7 @@ import Lucid import Lucid.Orphans () import Servant (ServerT) +import Control.Monad (void) import Data.Maybe (fromMaybe) import Data.Text.Display (display) import Distribution.Orphans () @@ -28,6 +29,7 @@ import FloraWeb.Server.Guards import FloraWeb.Server.Logging import FloraWeb.Session import FloraWeb.Templates +import FloraWeb.Templates.Packages.Changelog qualified as PackageChangelog import FloraWeb.Templates.Packages.Dependencies qualified as PackageDependencies import FloraWeb.Templates.Packages.Dependents qualified as PackageDependents import FloraWeb.Templates.Packages.Versions qualified as PackageVersions @@ -42,6 +44,8 @@ server = , showVersion = showVersionHandler , showDependents = showDependentsHandler , showDependencies = showDependenciesHandler + , showChangelog = showChangelogHandler + , showVersionChangelog = showVersionChangelogHandler , listVersions = listVersionsHandler } @@ -132,7 +136,7 @@ showDependentsHandler namespace packageName = do showDependenciesHandler :: Namespace -> PackageName -> FloraPage (Html ()) showDependenciesHandler namespace packageName = do - Log.logInfo_ $ display $ Prelude.show namespace + Log.logInfo_ $ display namespace session <- getSession templateEnv' <- fromSession session defaultTemplateEnv package <- guardThatPackageExists namespace packageName @@ -160,6 +164,28 @@ showDependenciesHandler namespace packageName = do ("Dependencies of " <> display namespace <> "/" <> display packageName) latestReleasedependencies +showChangelogHandler :: Namespace -> PackageName -> FloraPage (Html ()) +showChangelogHandler namespace packageName = do + package <- guardThatPackageExists namespace packageName + releases <- Query.getAllReleases (package.packageId) + let latestRelease = maximumBy (compare `on` version) releases + showVersionChangelogHandler namespace packageName (latestRelease.version) + +showVersionChangelogHandler :: Namespace -> PackageName -> Version -> FloraPage (Html ()) +showVersionChangelogHandler namespace packageName version = do + Log.logInfo_ $ display namespace + session <- getSession + templateEnv' <- fromSession session defaultTemplateEnv + void $ guardThatPackageExists namespace packageName + release <- guardThatReleaseExists namespace packageName version + let templateEnv = + templateEnv' + { title = display namespace <> "/" <> display packageName + , description = "Changelog of @" <> display namespace <> display packageName + } + + render templateEnv $ PackageChangelog.showChangelog namespace packageName version (release.changelog) + listVersionsHandler :: Namespace -> PackageName -> FloraPage (Html ()) listVersionsHandler namespace packageName = do session <- getSession diff --git a/src/FloraWeb/Templates/Admin.hs b/src/FloraWeb/Templates/Admin.hs index cfee24d0..098b5bf5 100644 --- a/src/FloraWeb/Templates/Admin.hs +++ b/src/FloraWeb/Templates/Admin.hs @@ -35,20 +35,11 @@ dataReport adminReport = do div_ [class_ cardClass] $ do dt_ [class_ "text-sm font-medium truncate"] - "README fetching" + "README, CHANGELOG, Upload time…" dd_ [class_ "mt-1 text-3xl font-semibold"] $ - form_ [action_ "/admin/readmes", method_ "POST"] $ do - button_ [class_ ""] "Schedule" - - div_ [class_ cardClass] $ do - dt_ - [class_ "text-sm font-medium truncate"] - "Fetch upload times for releases" - - dd_ [class_ "mt-1 text-3xl font-semibold"] $ - form_ [action_ "/admin/upload-times", method_ "POST"] $ do - button_ [class_ ""] "Schedule" + form_ [action_ "/admin/metadata", method_ "POST"] $ do + button_ [class_ ""] "Fetch release metadata" div_ [class_ cardClass] $ do dt_ diff --git a/src/FloraWeb/Templates/Packages/Changelog.hs b/src/FloraWeb/Templates/Packages/Changelog.hs new file mode 100644 index 00000000..2c7b6e88 --- /dev/null +++ b/src/FloraWeb/Templates/Packages/Changelog.hs @@ -0,0 +1,24 @@ +module FloraWeb.Templates.Packages.Changelog where + +import Data.Text (Text) +import Data.Text.Display (display) +import Distribution.Types.Version (Version) +import Flora.Model.Package (Namespace, PackageName) +import Flora.Model.Release.Types (TextHtml (..)) +import FloraWeb.Templates (FloraHTML) +import Lucid +import Lucid.Base (relaxHtmlT) + +showChangelog :: Namespace -> PackageName -> Version -> Maybe TextHtml -> FloraHTML +showChangelog namespace packageName version mChangelog = do + div_ [class_ "container"] $ do + div_ [class_ "divider"] $ do + div_ [class_ "page-title"] $ + h1_ [class_ ""] $ do + span_ [class_ "headline"] $ toHtml ("Changelog of " <> display namespace <> "/" <> display packageName) + toHtmlRaw @Text " " + span_ [class_ "dark:text-gray-200 version"] $ toHtml $ display version + section_ [class_ "release-changelog"] $ do + case mChangelog of + Nothing -> toHtml @Text "This release does not have a Changelog" + Just (MkTextHtml changelogText) -> relaxHtmlT changelogText diff --git a/src/FloraWeb/Templates/Pages/Packages.hs b/src/FloraWeb/Templates/Pages/Packages.hs index 26ada7be..0bea134e 100644 --- a/src/FloraWeb/Templates/Pages/Packages.hs +++ b/src/FloraWeb/Templates/Pages/Packages.hs @@ -74,7 +74,7 @@ presentationHeader release namespace name synopsis = do div_ [class_ "divider"] $ do div_ [class_ "page-title"] $ h1_ [class_ "package-title text-center tracking-tight"] $ do - span_ [class_ "headline"] $ toHtml namespace <> "/" <> toHtml name + span_ [class_ "headline"] $ toHtml (display namespace) <> "/" <> toHtml name span_ [class_ "dark:text-gray-200 version"] $ displayReleaseVersion release.version div_ [class_ "synopsis lg:text-xl text-center"] $ p_ [class_ ""] (toHtml synopsis) @@ -106,10 +106,10 @@ packageBody ul_ [class_ "package-left-rows grid-rows-3 md:sticky md:top-28"] $ do displayCategories categories displayLicense (metadata.license) - displayLinks packageName latestRelease metadata + displayLinks namespace packageName latestRelease metadata displayVersions namespace packageName packageReleases numberOfReleases - div_ [class_ "package-readme-column grow"] $ do - div_ [class_ "grid-rows-3 package-readme"] $ do + div_ [class_ "release-readme-column grow"] $ do + div_ [class_ "grid-rows-3 release-readme"] $ do displayReadme latestRelease div_ [class_ "package-right-column md:max-w-xs"] $ do ul_ [class_ "package-right-rows grid-rows-3 md:sticky md:top-28"] $ do @@ -142,19 +142,24 @@ displayCategories categories = do ul_ [class_ "categories"] $ do foldMap renderCategory categories -displayLinks :: PackageName -> Release -> ReleaseMetadata -> FloraHTML -displayLinks packageName _release meta@ReleaseMetadata{..} = do +displayLinks :: Namespace -> PackageName -> Release -> ReleaseMetadata -> FloraHTML +displayLinks namespace packageName release meta@ReleaseMetadata{..} = do li_ [class_ "mb-5"] $ do h3_ [class_ "lg:text-2xl package-body-section links mb-3"] "Links" ul_ [class_ "links"] $ do li_ [class_ "package-link"] $ a_ [href_ (getHomepage meta)] "Homepage" li_ [class_ "package-link"] $ a_ [href_ ("https://hackage.haskell.org/package/" <> display packageName)] "Documentation" li_ [class_ "package-link"] $ displaySourceRepos sourceRepos + li_ [class_ "package-link"] $ displayChangelog namespace packageName release.version release.changelog displaySourceRepos :: [Text] -> FloraHTML displaySourceRepos [] = toHtml @Text "No source repository" displaySourceRepos x = a_ [href_ (head x)] "Source repository" +displayChangelog :: Namespace -> PackageName -> Version -> Maybe TextHtml -> FloraHTML +displayChangelog _ _ _ Nothing = toHtml @Text "" +displayChangelog namespace packageName version (Just _) = a_ [href_ ("/" <> toUrlPiece (Links.packageVersionChangelog namespace packageName version))] "Changelog" + displayVersions :: Namespace -> PackageName -> Vector Release -> Word -> FloraHTML displayVersions namespace packageName versions numberOfReleases = li_ [class_ "mb-5"] $ do diff --git a/test/Flora/OddJobSpec.hs b/test/Flora/OddJobSpec.hs index 62eb0ffb..52e4229d 100644 --- a/test/Flora/OddJobSpec.hs +++ b/test/Flora/OddJobSpec.hs @@ -1,29 +1,14 @@ -{-# OPTIONS_GHC -Wno-unused-imports #-} - module Flora.OddJobSpec where -import Control.Monad -import Control.Monad.IO.Class -import Control.Monad.Trans.Reader import Data.Aeson -import Data.Password.Argon2 (mkPassword) import Data.UUID -import Database.PostgreSQL.Entity.DBT import Distribution.Types.Version -import Optics.Core -import Servant.Server import Test.Tasty import Test.Tasty.HUnit -import Flora.Environment import Flora.Model.Package.Types import Flora.Model.Release.Types -import Flora.Model.User -import Flora.Model.User.Query import Flora.OddJobs -import Flora.TestUtils -import FloraWeb.Client as Client -import FloraWeb.Routes.Pages.Sessions -- TODO aeson roundtrip tests spec :: TestTree @@ -32,13 +17,13 @@ spec = "odd-job" [ testCase "Readme aeson fixture" $ encode - ( MkReadme - ( MkReadmePayload + ( FetchReadme + ( ReadmeJobPayload { mpPackage = PackageName "uwu" , mpReleaseId = ReleaseId $ fromWords 12 13 14 15 , mpVersion = MkIntAesonVersion (mkVersion [1, 2, 3]) } ) ) - @?= "{\"contents\":{\"mpPackage\":\"uwu\",\"mpReleaseId\":\"0000000c-0000-000d-0000-000e0000000f\",\"mpVersion\":[1,2,3]},\"tag\":\"MkReadme\"}" + @?= "{\"contents\":{\"mpPackage\":\"uwu\",\"mpReleaseId\":\"0000000c-0000-000d-0000-000e0000000f\",\"mpVersion\":[1,2,3]},\"tag\":\"FetchReadme\"}" ] From a20429d41a7e803d5c6a64d6a9e4a91445c61520 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Thu, 13 Oct 2022 14:26:25 +0200 Subject: [PATCH 13/29] [FLORA-208] Packages are no longer kept as their own dependent --- CHANGELOG.md | 1 + migrations/20211113195849_create_dependents.sql | 3 ++- test/Flora/PackageSpec.hs | 16 ++++++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52c345d8..9c5caeba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * Enable the use of markdown extensions in package READMEs (#236) * Autofocus the search field on the home page (#235) * Support release changelogs (#241) +* Packages are no longer kept as their own dependent (#242) ## v1.0.4 -- 2022-10-02 diff --git a/migrations/20211113195849_create_dependents.sql b/migrations/20211113195849_create_dependents.sql index 742ee9b0..53297153 100644 --- a/migrations/20211113195849_create_dependents.sql +++ b/migrations/20211113195849_create_dependents.sql @@ -22,7 +22,8 @@ create materialized view dependents ( inner join "releases" as r1 on r1."package_id" = p0."package_id" inner join "package_components" as pc2 on pc2."release_id" = r1."release_id" inner join "requirements" as r3 on r3."package_component_id" = pc2."package_component_id" - inner join "packages" as p4 on p4."package_id" = r3."package_id"; + inner join "packages" as p4 on p4."package_id" = r3."package_id" + where (p4.name != p0.name) and (p4.namespace != p0.namespace); create index on dependents (name, dependent_id); create unique index on dependents (name, namespace, dependent_id); diff --git a/test/Flora/PackageSpec.hs b/test/Flora/PackageSpec.hs index 5dff68bd..16730f12 100644 --- a/test/Flora/PackageSpec.hs +++ b/test/Flora/PackageSpec.hs @@ -34,6 +34,7 @@ spec _fixtures = , testThis "@hackage/semigroups belongs to appropriate categories" testThatSemigroupsIsInMathematicsAndDataStructures , testThis "The \"haskell\" namespace has the correct number of packages" testCorrectNumberInHaskellNamespace , testThis "@haskell/bytestring has the correct number of dependents" testBytestringDependents + , testThis "Packages are not shown as their own dependent" testNoSelfDependent , testThis "Searching for `text` returns unique results by namespace/package name" testSearchResultUnicity , testThis "@hackage/time has the correct number of components of each type" testTimeComponents , testThis "@hackage/time components have the correct conditions in their metadata" testTimeConditions @@ -98,6 +99,21 @@ testBytestringDependents = do 6 (Vector.length results) +testNoSelfDependent :: TestEff () +testNoSelfDependent = do + results <- Query.getPackageDependents (Namespace "haskell") (PackageName "text") + let resultSet = Set.fromList . fmap (view #name) $ Vector.toList results + assertEqual + resultSet + ( Set.fromList + [ PackageName "flora" + , PackageName "hashable" + , PackageName "jose" + , PackageName "semigroups" + , PackageName "xml" + ] + ) + testBytestringDependencies :: TestEff () testBytestringDependencies = do bytestring <- fromJust <$> Query.getPackageByNamespaceAndName (Namespace "haskell") (PackageName "bytestring") From 297403fd37a5dd88d27a5ae68c6a75d366e4651c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Thu, 13 Oct 2022 15:20:08 +0200 Subject: [PATCH 14/29] [FLORA-243] Only show library dependencies that are fully imported (#244) * [FLORA-243] Only show library dependencies that are fully imported * Update Flora fixture * lint * add changelog entry --- CHANGELOG.md | 1 + migrations/20211106123053_create_releases.sql | 19 +- migrations/20221011215519_add_changelogs.sql | 22 -- src/Flora/Model/Package/Query.hs | 12 +- test/fixtures/Cabal/flora.cabal | 292 +++++++++++++----- 5 files changed, 235 insertions(+), 111 deletions(-) delete mode 100644 migrations/20221011215519_add_changelogs.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c5caeba..baf2e432 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * Autofocus the search field on the home page (#235) * Support release changelogs (#241) * Packages are no longer kept as their own dependent (#242) +* Show library dependencies only (#244) ## v1.0.4 -- 2022-10-02 diff --git a/migrations/20211106123053_create_releases.sql b/migrations/20211106123053_create_releases.sql index 44d30457..e61af51c 100644 --- a/migrations/20211106123053_create_releases.sql +++ b/migrations/20211106123053_create_releases.sql @@ -1,4 +1,4 @@ -create type readme_status as enum ('imported', 'inexistent', 'not-imported'); +create type import_status as enum ('imported', 'inexistent', 'not-imported'); -- A release belongs to a package, and contains multiple components. create table if not exists releases ( @@ -11,10 +11,25 @@ create table if not exists releases ( created_at timestamptz not null, updated_at timestamptz not null, readme text, - readme_status readme_status not null + readme_status import_status not null, + changelog text, + changelog_status import_status, + constraint consistent_readme_status + check ( + ((readme_status = 'imported' or readme_status = 'inexistent') + and readme is not null) + or (readme_status = 'not-imported' and readme is null) + ), + constraint consistent_changelog_status + check ( + ((changelog_status = 'imported' or changelog_status = 'inexistent') + and changelog is not null) + or (changelog_status = 'not-imported' and changelog is null) + ) ); create index on releases(package_id); create index on releases(uploaded_at); create index on releases(readme_status); create unique index on releases(package_id, version); +create index on releases(changelog_status); diff --git a/migrations/20221011215519_add_changelogs.sql b/migrations/20221011215519_add_changelogs.sql deleted file mode 100644 index dce8b787..00000000 --- a/migrations/20221011215519_add_changelogs.sql +++ /dev/null @@ -1,22 +0,0 @@ -alter type readme_status - rename to import_status; - -alter table releases - add column changelog text, - add column changelog_status import_status; - -alter table releases - add constraint consistent_readme_status - check ( - ((readme_status = 'imported' or readme_status = 'inexistent') - and readme is not null) - or (readme_status = 'not-imported' and readme is null) - ), - add constraint consistent_changelog_status - check ( - ((changelog_status = 'imported' or changelog_status = 'inexistent') - and changelog is not null) - or (changelog_status = 'not-imported' and changelog is null) - ); - -create index on releases(changelog_status); diff --git a/src/Flora/Model/Package/Query.hs b/src/Flora/Model/Package/Query.hs index 81f96410..cda5bfa4 100644 --- a/src/Flora/Model/Package/Query.hs +++ b/src/Flora/Model/Package/Query.hs @@ -235,7 +235,7 @@ getAllRequirementsQuery = from requirements as r0 inner join packages as p0 on p0.package_id = r0.package_id inner join package_components as p1 on p1.package_component_id = r0.package_component_id - and (p1.component_type = 'library' or p1.component_type = 'executable') + and (p1.component_type = 'library') inner join releases as r1 on r1.release_id = p1.release_id where r1.release_id = ? ) @@ -260,7 +260,7 @@ getRequirementsQuery = select distinct dependency.namespace, dependency.name, req.requirement from requirements as req inner join packages as dependency on dependency.package_id = req.package_id inner join package_components as pc ON pc.package_component_id = req.package_component_id - and (pc.component_type = 'library' or pc.component_type = 'executable') + and (pc.component_type = 'library') inner join releases as rel on rel.release_id = pc.release_id where rel."release_id" = ? order by dependency.namespace desc @@ -278,8 +278,12 @@ numberOfPackageRequirementsQuery = [sql| select distinct count(*) from requirements as req - inner join packages as dependency on dependency.package_id = req.package_id - inner join package_components as pc ON pc.package_component_id = req.package_component_id and pc.component_type = 'library' + inner join packages as dependency + on dependency.package_id = req.package_id + and dependency.status = 'fully-imported' + inner join package_components as pc + on pc.package_component_id = req.package_component_id + and pc.component_type = 'library' inner join releases as rel on rel.release_id = pc.release_id where rel."release_id" = ? |] diff --git a/test/fixtures/Cabal/flora.cabal b/test/fixtures/Cabal/flora.cabal index af4eb29e..22fceb48 100644 --- a/test/fixtures/Cabal/flora.cabal +++ b/test/fixtures/Cabal/flora.cabal @@ -1,152 +1,267 @@ cabal-version: 3.0 name: flora -version: 0.0.1.0 -homepage: https://github.com/flora/server#readme -bug-reports: https://github.com/flora/server/issues +version: 1.0.4 +homepage: https://github.com/flora-pm/flora-server/#readme +bug-reports: https://github.com/flora-pm/flora-server/issues author: Théophile Choutri maintainer: Théophile Choutri -license: MIT +license: BSD-3-Clause build-type: Simple extra-source-files: CHANGELOG.md - LICENSE.md + LICENSE README.md source-repository head type: git - location: https://github.com/flora/server + location: https://github.com/flora-pm/flora-server + +flag prod + description: + Compile the project with additional optimisations (takes longer) + + default: False + manual: True common common-extensions default-extensions: - NoMonomorphismRestriction NoStarIsType - ConstraintKinds DataKinds DeriveAnyClass - DeriveGeneric DerivingStrategies DerivingVia DuplicateRecordFields - FlexibleContexts - FlexibleInstances - GeneralizedNewtypeDeriving - InstanceSigs - KindSignatures LambdaCase - MultiParamTypeClasses - NamedFieldPuns OverloadedLabels OverloadedStrings - RankNTypes + PolyKinds + QuasiQuotes RecordWildCards - ScopedTypeVariables - StandaloneDeriving StrictData - TypeApplications - TypeOperators + TypeFamilies + UndecidableInstances ViewPatterns + OverloadedRecordDot - default-language: Haskell2010 + default-language: GHC2021 common common-ghc-options ghc-options: - -Wall -Wcompat -Werror -Widentities -Wincomplete-record-updates + -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wpartial-fields -Wredundant-constraints - -fhide-source-paths -Wno-unused-do-bind + -fhide-source-paths -Wno-unused-do-bind -fshow-hole-constraints + -fprint-potential-instances -Wno-unticked-promoted-constructors + -Werror=unused-imports + + if flag(prod) + ghc-options: + -flate-specialise -funbox-strict-fields + -finline-generics-aggressively -fexpose-all-unfoldings common common-rts-options - ghc-options: -rtsopts -threaded -with-rtsopts "-N -T" + ghc-options: -threaded "-with-rtsopts=-N -T" library import: common-extensions import: common-ghc-options + extra-libraries: stdc++ + cxx-options: -std=c++17 -Wall -D__EMBEDDED_SOUFFLE__ + cxx-sources: cbits/categorise.cpp + + -- pkgconfig-depends: libpq -any hs-source-dirs: src exposed-modules: Data.Aeson.Orphans + Data.Text.Display.Orphans + Data.Time.Orphans + Distribution.Orphans Flora.Environment + Flora.Environment.Config + Flora.Environment.OddJobs + Flora.Import.Categories + Flora.Import.Categories.Tuning Flora.Import.Package + Flora.Import.Package.Bulk Flora.Import.Types + Flora.Model.Admin.Report Flora.Model.Category + Flora.Model.Category.Query + Flora.Model.Category.Types + Flora.Model.Category.Update + Flora.Model.Downloads Flora.Model.Organisation Flora.Model.Package Flora.Model.Package.Component Flora.Model.Package.Orphans + Flora.Model.Package.Publisher + Flora.Model.Package.Query Flora.Model.Package.Types + Flora.Model.Package.Update + Flora.Model.PersistentSession Flora.Model.Release - Flora.Model.Release.Orphans + Flora.Model.Release.Query + Flora.Model.Release.Types + Flora.Model.Release.Update Flora.Model.Requirement Flora.Model.User + Flora.Model.User.Orphans + Flora.Model.User.Query + Flora.Model.User.Update + Flora.OddJobs + Flora.OddJobs.Render + Flora.OddJobs.Types Flora.Publish + Flora.Search Flora.ThirdParties.Hackage.API Flora.ThirdParties.Hackage.Client + FloraWeb.Autoreload + FloraWeb.Client + FloraWeb.Components.CategoryCard + FloraWeb.Components.Footer + FloraWeb.Components.Header + FloraWeb.Components.Navbar + FloraWeb.Components.PackageListHeader + FloraWeb.Components.PackageListItem + FloraWeb.Components.PaginationNav + FloraWeb.Components.Utils + FloraWeb.Components.VersionListHeader + FloraWeb.Components.VersionListItem + FloraWeb.Links + FloraWeb.Routes + FloraWeb.Routes.Pages + FloraWeb.Routes.Pages.Admin + FloraWeb.Routes.Pages.Categories + FloraWeb.Routes.Pages.Packages + FloraWeb.Routes.Pages.Search + FloraWeb.Routes.Pages.Sessions FloraWeb.Server FloraWeb.Server.Auth - FloraWeb.Server.Logging.Metrics - FloraWeb.Server.Logging.Tracing + FloraWeb.Server.Auth.Types + FloraWeb.Server.Guards + FloraWeb.Server.Logging + FloraWeb.Server.Metrics + FloraWeb.Server.OpenSearch FloraWeb.Server.Pages + FloraWeb.Server.Pages.Admin + FloraWeb.Server.Pages.Categories FloraWeb.Server.Pages.Packages + FloraWeb.Server.Pages.Search + FloraWeb.Server.Pages.Sessions + FloraWeb.Server.Tracing + FloraWeb.Server.Utils + FloraWeb.Session FloraWeb.Templates + FloraWeb.Templates.Admin + FloraWeb.Templates.Admin.Packages + FloraWeb.Templates.Admin.Users FloraWeb.Templates.Error - FloraWeb.Templates.Layout.App + FloraWeb.Templates.Packages.Changelog + FloraWeb.Templates.Packages.Dependencies + FloraWeb.Templates.Packages.Dependents + FloraWeb.Templates.Packages.Listing + FloraWeb.Templates.Packages.Versions + FloraWeb.Templates.Pages.Categories + FloraWeb.Templates.Pages.Categories.Index + FloraWeb.Templates.Pages.Categories.Show FloraWeb.Templates.Pages.Home FloraWeb.Templates.Pages.Packages + FloraWeb.Templates.Pages.Search + FloraWeb.Templates.Pages.Sessions FloraWeb.Templates.Types FloraWeb.Types + Log.Backend.File Lucid.Orphans build-depends: - , aeson <=1.6 + , aeson <2.1.0 , async ^>=2.2 - , base ^>=4.14 + , base ^>=4.16 , blaze-builder - , bytestring ^>=0.10 - , Cabal ^>=3.6 + , bytestring >=0.10 && <0.12 + , Cabal-syntax ^>=3.8 , clock ^>=0.8 , cmark-gfm ^>=0.2 , colourista ^>=0.1 + , commonmark + , commonmark-extensions , containers ^>=0.6 , cookie ^>=0.4 - , cryptohash-sha256 ^>=0.11 + , cryptohash ^>=0.11 + , data-default + , data-default-class , directory ^>=1.3 + , effectful + , effectful-core , envparse ^>=0.5 - , hashable ^>=1.3 + , filepath ^>=1.4 + , http-api-data ^>=0.4 + , http-client ^>=0.7.10 + , http-client-tls + , http-media , http-types ^>=0.12 - , lucid + , iso8601-time ^>=0.1 + , lens + , log-base ^>=0.12 + , log-effectful ^>=1.0 + , lucid ^>=2.11 + , lucid-alpine ^>=0.1 + , lucid-aria ^>=0.1 , lucid-svg ^>=0.7 + , monad-control ^>=1.0 + , monad-time ^>=0.4 , mtl ^>=2.2 + , odd-jobs , optics-core ^>=0.4 , optparse-applicative ^>=0.16 , password ^>=3.0 , password-types ^>=1.0 - , pcre2 ^>=2.0 + , pcre2 , pg-entity ^>=0.0 , pg-transact ^>=0.3 + , pg-transact-effectful , postgresql-simple ^>=0.6 , pretty ^>=1.1 + , pretty-simple ^>=4.0 , prometheus-client ^>=1.1 , prometheus-metrics-ghc ^>=1.0 , prometheus-proc ^>=0.1 - , PyF ^>=0.10 + , PyF ^>=0.11 , raven-haskell ^>=0.1 - , resource-pool ^>=0.2 - , servant ^>=0.18 - , servant-client ^>=0.18 - , servant-client-core ^>=0.18 + , resource-pool >=0.3 && <0.4.0.0 + , safe-exceptions + , servant ^>=0.19 + , servant-client ^>=0.19 + , servant-client-core ^>=0.19 + , servant-effectful , servant-lucid ^>=0.9 - , servant-server ^>=0.18 - , text ^>=1.2 - , text-display ^>=0.0 - , time ^>=1.9 + , servant-server ^>=0.19 + , servant-websockets ^>=2.0 + , slugify ^>=0.1 + , souffle-haskell >=3.4 && <3.6 + , split ^>=0.2 + , streaming ^>=0.2 + , template-haskell + , text + , text-display + , time + , time-effectful , transformers ^>=0.5 + , transformers-base ^>=0.4 + , typed-process ^>=0.2 + , unliftio-core + , unordered-containers ^>=0.2 , uuid ^>=1.3 - , vector ^>=0.12 + , vector ^>=0.13 + , vector-algorithms ^>=0.9 , wai ^>=3.2 - , wai-cors ^>=0.2 - , wai-logger ^>=2.3 + , wai-log ^>=0.3 , wai-middleware-heartbeat ^>=0.0 , wai-middleware-prometheus ^>=1.0 - , wai-middleware-static ^>=0.9 , warp ^>=3.3 + , websockets ^>=0.12 + , wreq >=0.5.3.3 + , xml-conduit + , xml-conduit-writer executable flora-server import: common-extensions @@ -163,76 +278,87 @@ executable flora-cli import: common-ghc-options import: common-rts-options main-is: Main.hs - other-modules: CoverageReport + other-modules: DesignSystem hs-source-dirs: app/cli build-depends: , ansi-terminal , base - , Cabal + , bytestring + , Cabal-syntax , colourista , directory + , effectful + , effectful-core , flora - , flora-test-fixtures - , optparse-applicative ^>=0.16 + , http-client + , log-base + , log-effectful + , lucid + , monad-control + , optics-core + , optparse-applicative ^>=0.16 + , password-types , pg-entity , pg-transact + , pg-transact-effectful , postgresql-simple - , postgresql-simple-migration + , PyF , text , text-display + , time-effectful + , transformers , typed-process , uuid - -library flora-test-fixtures - import: common-extensions - import: common-ghc-options - hs-source-dirs: test/fixtures - build-depends: - , aeson - , base - , Cabal - , flora - , optics-core - , password - , pg-entity - , time - , uuid , vector - exposed-modules: - Flora.PackageFixtures - -executable flora-test +test-suite flora-test import: common-extensions import: common-ghc-options import: common-rts-options + type: exitcode-stdio-1.0 main-is: Main.hs hs-source-dirs: test build-depends: , aeson - , aeson-pretty , base - , Cabal + , bytestring + , Cabal-syntax , containers + , effectful-core + , exceptions , flora - , flora-test-fixtures - , hspec - , hspec-expectations-lifted - , hspec-pg-transact + , hedgehog + , http-client + , log-base + , log-effectful + , network-uri , optics-core , password , pg-entity , pg-transact + , pg-transact-effectful , postgresql-migration , postgresql-simple + , pretty-simple , resource-pool - , string-qq + , servant + , servant-client + , servant-client-core + , servant-server + , tasty + , tasty-hunit + , text , time + , time-effectful + , transformers , uuid , vector - , vector-algorithms ^>=0.8 other-modules: + Flora.CabalSpec + Flora.CategorySpec + Flora.OddJobSpec Flora.PackageSpec + Flora.TemplateSpec + Flora.TestUtils Flora.UserSpec - SpecHelpers From aa12ec1d51b59167ac6ee4d680905cdca82bd440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Thu, 13 Oct 2022 16:17:08 +0200 Subject: [PATCH 15/29] [FLORA-185] Display license in package listings --- CHANGELOG.md | 1 + app/cli/DesignSystem.hs | 3 +- assets/css/app.css | 34 +++++++++++---- assets/css/variables.css | 6 ++- .../20220326182257_create_latest_versions.sql | 6 ++- src/Flora/Model/Package/Query.hs | 24 +++++++---- src/Flora/Search.hs | 11 ++--- src/FloraWeb/Components/PackageListItem.hs | 41 +++++++++++++++++-- .../Templates/Packages/Dependencies.hs | 3 +- src/FloraWeb/Templates/Packages/Dependents.hs | 3 +- src/FloraWeb/Templates/Packages/Listing.hs | 25 +++++------ .../Templates/Pages/Categories/Show.hs | 3 +- src/FloraWeb/Templates/Pages/Search.hs | 5 ++- 13 files changed, 114 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index baf2e432..735d340f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * Support release changelogs (#241) * Packages are no longer kept as their own dependent (#242) * Show library dependencies only (#244) +* Show licenses in package listings (#245) ## v1.0.4 -- 2022-10-02 diff --git a/app/cli/DesignSystem.hs b/app/cli/DesignSystem.hs index c776ef7d..ff53933f 100644 --- a/app/cli/DesignSystem.hs +++ b/app/cli/DesignSystem.hs @@ -88,7 +88,8 @@ packageListItemExample = ( namespaceExample , packageNameExample , "Basic libraries" - , "4.16.0.0" + , read "4.16.0.0" + , read "BSD-3-Clause" ) categoryCardExample :: FloraHTML diff --git a/assets/css/app.css b/assets/css/app.css index 3fb65bbe..6a52dc23 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -314,19 +314,35 @@ div[class="bullets"] { color: var(--package-list-item-name-color); } - .package-list-item__version { - margin-top: 0.5rem; - font-size: 0.875rem; - line-height: 1.25rem; + .package-list-item__synopsis { + display: inline; + color: var(--package-list-item-synopsis-color); + } + + .package-list-item__metadata { + color: var(--package-list-item-metadata-color); - --tw-text-opacity: 1; + .package-list-item__license { + color: var(--package-list-item-metadata-color); + } - color: var(--package-list-item-version-color); + .package-list-item__version { + color: var(--package-list-item-version-color); + } + + .license-icon { + height: 1.25rem; + width: 1.25rem; + display: inline; + margin-top: -0.25rem; + } } - .package-list-item__synopsis { - display: inline; - color: var(--package-list-item-synopsis-color); + .package-list-item__metadata > * { + margin-top: 0.5rem; + margin-right: 10px; + font-size: 0.875rem; + line-height: 1.25rem; } } } diff --git a/assets/css/variables.css b/assets/css/variables.css index 7e25bd15..7df7abcf 100644 --- a/assets/css/variables.css +++ b/assets/css/variables.css @@ -24,7 +24,8 @@ --package-list-item-background-hover-color: hsl(220 13% 91%); --package-list-item-name-color: hsl(221 39% 11%); --package-list-item-synopsis-color: black; - --package-list-item-version-color: black; + --package-list-item-metadata-color: black; + --package-list-item-version-color: #0a6; --search-bar-color: hsl(221 39% 11%); --search-bar-background-color: white; --search-bar-background-hover-color: white; @@ -59,8 +60,9 @@ html[data-theme="dark"] { --text-color: hsl(216 12% 84%); --package-list-item-background-hover-color: hsl(218 30% 15%); --package-list-item-name-color: hsl(294 40% 60%); + --package-list-item-metadata-color: hsl(220 13% 91%); --package-list-item-synopsis-color: hsl(220 13% 91%); - --package-list-item-version-color: hsl(220 13% 91%); + --package-list-item-version-color: hsl(120 80% 50%); --search-bar-color: hsl(216 12% 84%); --search-bar-background-color: hsl(218 30% 25%); --search-bar-background-hover-color: hsl(218 30% 25%); diff --git a/migrations/20220326182257_create_latest_versions.sql b/migrations/20220326182257_create_latest_versions.sql index 52122d64..f15d33d2 100644 --- a/migrations/20220326182257_create_latest_versions.sql +++ b/migrations/20220326182257_create_latest_versions.sql @@ -3,17 +3,19 @@ create materialized view latest_versions ( name, synopsis, package_id, - version) as + version, + license) as select distinct on (p.package_id) p.namespace , p.name , r.metadata ->> 'synopsis' as synopsis , p.package_id , r.version + , r.metadata ->> 'license' as license from "packages" as p inner join "releases" as r on p."package_id" = r."package_id" where p.status = 'fully-imported' - group by p.namespace, p.name, synopsis, p.package_id, version + group by p.namespace, p.name, synopsis, p.package_id, r.version, license order by p.package_id, version desc; create index on latest_versions (namespace, name); diff --git a/src/Flora/Model/Package/Query.hs b/src/Flora/Model/Package/Query.hs index cda5bfa4..65585944 100644 --- a/src/Flora/Model/Package/Query.hs +++ b/src/Flora/Model/Package/Query.hs @@ -30,6 +30,7 @@ import Effectful.PostgreSQL.Transact.Effect import Effectful.Time import Log qualified +import Distribution.SPDX.License qualified as SPDX import Flora.Model.Category (Category, CategoryId) import Flora.Model.Category.Types (PackageCategory) import Flora.Model.Package (Namespace (..), Package, PackageId, PackageName) @@ -140,7 +141,7 @@ getAllPackageDependentsWithLatestVersion :: ([DB, Log, Time, IOE] :>> es) => Namespace -> PackageName - -> Eff es (Vector (Namespace, PackageName, Text, Version)) + -> Eff es (Vector (Namespace, PackageName, Text, Version, SPDX.License)) getAllPackageDependentsWithLatestVersion namespace packageName = dbtToEff $ query Select packageDependentsWithLatestVersionQuery (namespace, packageName) @@ -149,7 +150,7 @@ getPackageDependentsWithLatestVersion :: ([DB, Log, Time, IOE] :>> es) => Namespace -> PackageName - -> Eff es (Vector (Namespace, PackageName, Text, Version)) + -> Eff es (Vector (Namespace, PackageName, Text, Version, SPDX.License)) getPackageDependentsWithLatestVersion namespace packageName = do (result, duration) <- timeAction $ @@ -169,6 +170,7 @@ packageDependentsWithLatestVersionQuery = , p."name" , r.metadata ->> 'synopsis' as "synopsis" , max(r."version") + , r.metadata ->> 'license' as "license" FROM "packages" AS p INNER JOIN "dependents" AS dep ON p."package_id" = dep."dependent_id" @@ -176,7 +178,7 @@ packageDependentsWithLatestVersionQuery = ON r."package_id" = p."package_id" WHERE dep."namespace" = ? AND dep."name" = ? - GROUP BY (p.namespace, p.name, synopsis) + GROUP BY (p.namespace, p.name, synopsis, license) |] getComponentById :: ([DB, Log, Time, IOE] :>> es) => ComponentId -> Eff es (Maybe PackageComponent) @@ -209,7 +211,7 @@ getAllRequirements :: ([DB, Log, Time, IOE] :>> es) => ReleaseId -- ^ Id of the release for which we want the dependencies - -> Eff es (Vector (Namespace, PackageName, Text, Version, Text)) + -> Eff es (Vector (Namespace, PackageName, Text, Version, Text, SPDX.License)) -- ^ Returns a vector of (Namespace, Name, dependency requirement, version of latest of release of dependency, synopsis of dependency) getAllRequirements releaseId = dbtToEff $ query Select getAllRequirementsQuery (Only releaseId) @@ -244,7 +246,7 @@ getAllRequirementsQuery = , req.requirement , r3.version as "dependency_latest_version" , r3.metadata ->> 'synopsis' as "dependency_latest_synopsis" - -- , r3.metadata ->> 'license' as "dependency_latest_license" + , r3.metadata ->> 'license' as "dependency_latest_license" from requirements as req inner join packages as p2 on p2.namespace = req.namespace and p2.name = req.name inner join releases as r3 on r3.package_id = p2.package_id @@ -303,12 +305,12 @@ getPackageCategories packageId = getPackagesFromCategoryWithLatestVersion :: ([DB, Log, Time, IOE] :>> es) => CategoryId - -> Eff es (Vector (Namespace, PackageName, Text, Version)) + -> Eff es (Vector (Namespace, PackageName, Text, Version, SPDX.License)) getPackagesFromCategoryWithLatestVersion categoryId = dbtToEff $ query Select q (Only categoryId) where q = [sql| - select distinct lv.namespace, lv.name, lv.synopsis, lv.version from latest_versions as lv + select distinct lv.namespace, lv.name, lv.synopsis, lv.version, lv.license from latest_versions as lv inner join package_categories as pc on pc.package_id = lv.package_id inner join categories as c on c.category_id = pc.category_id where c.category_id = ? @@ -318,7 +320,7 @@ searchPackage :: ([DB, Log, Time, IOE] :>> es) => Word -> Text - -> Eff es (Vector (Namespace, PackageName, Text, Version, Float)) + -> Eff es (Vector (Namespace, PackageName, Text, Version, SPDX.License, Float)) searchPackage pageNumber searchString = dbtToEff $ let limit = 30 @@ -330,6 +332,7 @@ searchPackage pageNumber searchString = , lv."name" , lv."synopsis" , lv."version" + , lv."license" , word_similarity(lv.name, ?) as rating FROM latest_versions as lv WHERE ? <% lv.name @@ -338,6 +341,7 @@ searchPackage pageNumber searchString = , lv."name" , lv."synopsis" , lv."version" + , lv."license" ORDER BY rating desc, count(lv."namespace") desc, lv.name asc LIMIT 30 OFFSET ? @@ -348,7 +352,7 @@ searchPackage pageNumber searchString = listAllPackages :: ([DB, Log, Time, IOE] :>> es) => Word - -> Eff es (Vector (Namespace, PackageName, Text, Version, Float)) + -> Eff es (Vector (Namespace, PackageName, Text, Version, SPDX.License, Float)) listAllPackages pageNumber = dbtToEff $ let limit = 30 @@ -360,6 +364,7 @@ listAllPackages pageNumber = , lv."name" , lv."synopsis" , lv."version" + , lv."license" , (1.0::real) as rating FROM latest_versions as lv GROUP BY @@ -367,6 +372,7 @@ listAllPackages pageNumber = , lv."name" , lv."synopsis" , lv."version" + , lv."license" ORDER BY rating desc, count(lv."namespace") desc, lv.name asc LIMIT 30 OFFSET ? diff --git a/src/Flora/Search.hs b/src/Flora/Search.hs index 2448bedb..3d414686 100644 --- a/src/Flora/Search.hs +++ b/src/Flora/Search.hs @@ -10,6 +10,7 @@ import Data.Vector qualified as Vector import Distribution.Types.Version (Version) import Log qualified +import Distribution.SPDX.License qualified as SPDX import Flora.Model.Package (Namespace (..), PackageName, formatPackage) import Flora.Model.Package.Query qualified as Query import FloraWeb.Server.Auth (FloraPage) @@ -25,7 +26,7 @@ instance Display SearchAction where displayBuilder ListAllPackages = "Packages" displayBuilder (SearchPackages title) = "\"" <> Builder.fromText title <> "\"" -searchPackageByName :: Word -> Text -> FloraPage (Word, Vector (Namespace, PackageName, Text, Version)) +searchPackageByName :: Word -> Text -> FloraPage (Word, Vector (Namespace, PackageName, Text, Version, SPDX.License)) searchPackageByName pageNumber queryString = do (dbResults, duration) <- timeAction $ Query.searchPackage pageNumber queryString @@ -36,7 +37,7 @@ searchPackageByName pageNumber queryString = do , "results_count" .= Vector.length dbResults , "results" .= List.map - ( \(namespace, packageName, _, _, score :: Float) -> + ( \(namespace, packageName, _, _, _, score :: Float) -> object [ "package" .= formatPackage namespace packageName , "score" .= score @@ -45,15 +46,15 @@ searchPackageByName pageNumber queryString = do (Vector.toList dbResults) ] - let getInfo = (,,,) <$> view _1 <*> view _2 <*> view _3 <*> view _4 + let getInfo = (,,,,) <$> view _1 <*> view _2 <*> view _3 <*> view _4 <*> view _5 count <- Query.countPackagesByName queryString let results = fmap getInfo dbResults pure (count, results) -listAllPackages :: Word -> FloraPage (Word, Vector (Namespace, PackageName, Text, Version)) +listAllPackages :: Word -> FloraPage (Word, Vector (Namespace, PackageName, Text, Version, SPDX.License)) listAllPackages pageNumber = do results <- Query.listAllPackages pageNumber count <- Query.countPackages - let getInfo = (,,,) <$> view _1 <*> view _2 <*> view _3 <*> view _4 + let getInfo = (,,,,) <$> view _1 <*> view _2 <*> view _3 <*> view _4 <*> view _5 let resultVector = fmap getInfo results pure (count, resultVector) diff --git a/src/FloraWeb/Components/PackageListItem.hs b/src/FloraWeb/Components/PackageListItem.hs index f7e88e13..8c7d5351 100644 --- a/src/FloraWeb/Components/PackageListItem.hs +++ b/src/FloraWeb/Components/PackageListItem.hs @@ -1,5 +1,6 @@ module FloraWeb.Components.PackageListItem ( packageListItem + , packageListItemWithVersionRange ) where @@ -8,10 +9,14 @@ import Data.Text.Display (display) import FloraWeb.Templates (FloraHTML) import Lucid +import Distribution.SPDX.License qualified as SPDX +import Distribution.Types.Version (Version) import Flora.Model.Package (Namespace, PackageName) +import Lucid.Orphans () +import Lucid.Svg (clip_rule_, d_, fill_, fill_rule_, path_, viewBox_) -packageListItem :: (Namespace, PackageName, Text, Text) -> FloraHTML -packageListItem (namespace, packageName, synopsis, version) = do +packageListItem :: (Namespace, PackageName, Text, Version, SPDX.License) -> FloraHTML +packageListItem (namespace, packageName, synopsis, version, license) = do let href = href_ ("/packages/" <> display namespace <> "/" <> display packageName) li_ [class_ "package-list-item"] $ a_ [href, class_ ""] $ do @@ -19,4 +24,34 @@ packageListItem (namespace, packageName, synopsis, version) = do strong_ [class_ ""] . toHtml $ display namespace <> "/" <> display packageName p_ [class_ "package-list-item__synopsis"] $ toHtml synopsis - div_ [class_ "package-list-item__version"] $ toHtml version + div_ [class_ "package-list-item__metadata"] $ do + span_ [class_ "package-list-item__license"] $ do + licenseIcon + toHtml license + span_ [class_ "package-list-item__version"] $ "v" <> toHtml version + +packageListItemWithVersionRange :: (Namespace, PackageName, Text, Text, SPDX.License) -> FloraHTML +packageListItemWithVersionRange (namespace, packageName, synopsis, versionRange, license) = do + let href = href_ ("/packages/" <> display namespace <> "/" <> display packageName) + li_ [class_ "package-list-item"] $ + a_ [href, class_ ""] $ do + h4_ [class_ "package-list-item__name"] $ + strong_ [class_ ""] . toHtml $ + display namespace <> "/" <> display packageName + p_ [class_ "package-list-item__synopsis"] $ toHtml synopsis + div_ [class_ "package-list-item__metadata"] $ do + span_ [class_ "package-list-item__license"] $ do + licenseIcon + toHtml license + displayVersionRange versionRange + +displayVersionRange :: Text -> FloraHTML +displayVersionRange versionRange = + if versionRange == ">=0" + then "" + else span_ [class_ "package-list-item__version-range"] $ toHtml versionRange + +licenseIcon :: FloraHTML +licenseIcon = + svg_ [xmlns_ "http://www.w3.org/2000/svg", viewBox_ "0 0 20 20", fill_ "currentColor", class_ "license-icon"] $ + path_ [fill_rule_ "evenodd", d_ "M10 2a.75.75 0 01.75.75v.258a33.186 33.186 0 016.668.83.75.75 0 01-.336 1.461 31.28 31.28 0 00-1.103-.232l1.702 7.545a.75.75 0 01-.387.832A4.981 4.981 0 0115 14c-.825 0-1.606-.2-2.294-.556a.75.75 0 01-.387-.832l1.77-7.849a31.743 31.743 0 00-3.339-.254v11.505a20.01 20.01 0 013.78.501.75.75 0 11-.339 1.462A18.558 18.558 0 0010 17.5c-1.442 0-2.845.165-4.191.477a.75.75 0 01-.338-1.462 20.01 20.01 0 013.779-.501V4.509c-1.129.026-2.243.112-3.34.254l1.771 7.85a.75.75 0 01-.387.83A4.98 4.98 0 015 14a4.98 4.98 0 01-2.294-.556.75.75 0 01-.387-.832L4.02 5.067c-.37.07-.738.148-1.103.232a.75.75 0 01-.336-1.462 32.845 32.845 0 016.668-.829V2.75A.75.75 0 0110 2zM5 7.543L3.92 12.33a3.499 3.499 0 002.16 0L5 7.543zm10 0l-1.08 4.787a3.498 3.498 0 002.16 0L15 7.543z", clip_rule_ "evenodd"] diff --git a/src/FloraWeb/Templates/Packages/Dependencies.hs b/src/FloraWeb/Templates/Packages/Dependencies.hs index f707fbbd..82286b2b 100644 --- a/src/FloraWeb/Templates/Packages/Dependencies.hs +++ b/src/FloraWeb/Templates/Packages/Dependencies.hs @@ -3,6 +3,7 @@ module FloraWeb.Templates.Packages.Dependencies where import Data.Text (Text) import Data.Vector (Vector) import Data.Vector qualified as Vector +import Distribution.SPDX.License qualified as SPDX import Distribution.Version (Version) import Flora.Model.Package (Namespace, PackageName) import FloraWeb.Components.PackageListHeader (presentationHeader) @@ -10,7 +11,7 @@ import FloraWeb.Templates (FloraHTML) import FloraWeb.Templates.Packages.Listing (packageListingWithRange) import Lucid -showDependencies :: Text -> Vector (Namespace, PackageName, Text, Version, Text) -> FloraHTML +showDependencies :: Text -> Vector (Namespace, PackageName, Text, Version, Text, SPDX.License) -> FloraHTML showDependencies searchString packagesInfo = do div_ [class_ "container"] $ do presentationHeader searchString "" (fromIntegral $ Vector.length packagesInfo) diff --git a/src/FloraWeb/Templates/Packages/Dependents.hs b/src/FloraWeb/Templates/Packages/Dependents.hs index f224878b..a4021738 100644 --- a/src/FloraWeb/Templates/Packages/Dependents.hs +++ b/src/FloraWeb/Templates/Packages/Dependents.hs @@ -3,6 +3,7 @@ module FloraWeb.Templates.Packages.Dependents where import Data.Text (Text) import Data.Vector (Vector) import Data.Vector qualified as Vector +import Distribution.SPDX.License qualified as SPDX import Distribution.Types.Version (Version) import Flora.Model.Package (Namespace, PackageName) import FloraWeb.Components.PackageListHeader (presentationHeader) @@ -10,7 +11,7 @@ import FloraWeb.Templates (FloraHTML) import FloraWeb.Templates.Packages.Listing import Lucid -showDependents :: Text -> Vector (Namespace, PackageName, Text, Version) -> FloraHTML +showDependents :: Text -> Vector (Namespace, PackageName, Text, Version, SPDX.License) -> FloraHTML showDependents searchString packagesInfo = div_ [class_ "container"] $ do presentationHeader searchString "" (fromIntegral $ Vector.length packagesInfo) diff --git a/src/FloraWeb/Templates/Packages/Listing.hs b/src/FloraWeb/Templates/Packages/Listing.hs index 67929ba6..64b3ea37 100644 --- a/src/FloraWeb/Templates/Packages/Listing.hs +++ b/src/FloraWeb/Templates/Packages/Listing.hs @@ -7,39 +7,34 @@ module FloraWeb.Templates.Packages.Listing where import Data.Text (Text) -import Data.Text.Display (display) import Data.Vector (Vector) import Data.Vector qualified as Vector import Distribution.Types.Version (Version) import Lucid import Distribution.Orphans () +import Distribution.SPDX.License qualified as SPDX import Flora.Model.Package -import FloraWeb.Components.PackageListItem (packageListItem) +import FloraWeb.Components.PackageListItem (packageListItem, packageListItemWithVersionRange) import FloraWeb.Templates (FloraHTML) -- | Render a list of package informations -packageListing :: Vector (Namespace, PackageName, Text, Version) -> FloraHTML +packageListing :: Vector (Namespace, PackageName, Text, Version, SPDX.License) -> FloraHTML packageListing packages = do ul_ [class_ "package-list space-y-2"] $ do Vector.forM_ packages $ \pInfo -> do showPackageWithVersion pInfo -packageListingWithRange :: Vector (Namespace, PackageName, Text, Version, Text) -> FloraHTML +packageListingWithRange :: Vector (Namespace, PackageName, Text, Version, Text, SPDX.License) -> FloraHTML packageListingWithRange packages = do ul_ [class_ "package-list space-y-2"] $ do Vector.forM_ packages $ \pInfo -> do showPackageWithRange pInfo -showPackageWithVersion :: (Namespace, PackageName, Text, Version) -> FloraHTML -showPackageWithVersion (namespace, name, synopsis, version) = - packageListItem (namespace, name, synopsis, display version) +showPackageWithVersion :: (Namespace, PackageName, Text, Version, SPDX.License) -> FloraHTML +showPackageWithVersion (namespace, name, synopsis, version, license) = + packageListItem (namespace, name, synopsis, version, license) -showPackageWithRange :: (Namespace, PackageName, Text, Version, Text) -> FloraHTML -showPackageWithRange (namespace, name, versionRange, _latestVersionOfDependency, synopsis) = - packageListItem (namespace, name, synopsis, range) - where - range = - if versionRange == ">=0" - then "" - else versionRange +showPackageWithRange :: (Namespace, PackageName, Text, Version, Text, SPDX.License) -> FloraHTML +showPackageWithRange (namespace, name, versionRange, _latestVersionOfDependency, synopsis, license) = + packageListItemWithVersionRange (namespace, name, synopsis, versionRange, license) diff --git a/src/FloraWeb/Templates/Pages/Categories/Show.hs b/src/FloraWeb/Templates/Pages/Categories/Show.hs index 28942040..cbb5f97c 100644 --- a/src/FloraWeb/Templates/Pages/Categories/Show.hs +++ b/src/FloraWeb/Templates/Pages/Categories/Show.hs @@ -3,6 +3,7 @@ module FloraWeb.Templates.Pages.Categories.Show where import Data.Text (Text) import Data.Vector (Vector) import Data.Vector qualified as V +import Distribution.SPDX.License qualified as SPDX import Distribution.Types.Version (Version) import Lucid @@ -12,7 +13,7 @@ import FloraWeb.Components.PackageListHeader (presentationHeader) import FloraWeb.Templates (FloraHTML) import FloraWeb.Templates.Packages.Listing (packageListing) -showCategory :: Category -> Vector (Namespace, PackageName, Text, Version) -> FloraHTML +showCategory :: Category -> Vector (Namespace, PackageName, Text, Version, SPDX.License) -> FloraHTML showCategory Category{name, synopsis} packagesInfo = do div_ [class_ "container"] $ do presentationHeader name synopsis (fromIntegral $ V.length packagesInfo) diff --git a/src/FloraWeb/Templates/Pages/Search.hs b/src/FloraWeb/Templates/Pages/Search.hs index 670e9cea..b2694dee 100644 --- a/src/FloraWeb/Templates/Pages/Search.hs +++ b/src/FloraWeb/Templates/Pages/Search.hs @@ -3,6 +3,7 @@ module FloraWeb.Templates.Pages.Search where import Control.Monad (when) import Data.Text (Text) import Data.Vector (Vector) +import Distribution.SPDX.License qualified as SPDX import Distribution.Types.Version (Version) import Flora.Model.Package (Namespace, PackageName) import Flora.Search (SearchAction (..)) @@ -12,14 +13,14 @@ import FloraWeb.Templates import FloraWeb.Templates.Packages.Listing (packageListing) import Lucid -showAllPackages :: Word -> Word -> Vector (Namespace, PackageName, Text, Version) -> FloraHTML +showAllPackages :: Word -> Word -> Vector (Namespace, PackageName, Text, Version, SPDX.License) -> FloraHTML showAllPackages count currentPage packagesInfo = do div_ [class_ "container"] $ do presentationHeader "Packages" "" count div_ [class_ "md:col-span-3"] $ packageListing packagesInfo paginationNav count currentPage ListAllPackages -showResults :: Text -> Word -> Word -> Vector (Namespace, PackageName, Text, Version) -> FloraHTML +showResults :: Text -> Word -> Word -> Vector (Namespace, PackageName, Text, Version, SPDX.License) -> FloraHTML showResults searchString count currentPage packagesInfo = do div_ [class_ "container"] $ do presentationHeader searchString "" count From be4d5b9cff65b1ddcd82687b86e39beb538ac89c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Thu, 13 Oct 2022 19:11:49 +0200 Subject: [PATCH 16/29] Remove old images folder --- images/flora-about.png | Bin 78756 -> 0 bytes images/flora-package-view.png | Bin 88912 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 images/flora-about.png delete mode 100644 images/flora-package-view.png diff --git a/images/flora-about.png b/images/flora-about.png deleted file mode 100644 index 02cb66eb0d90500479105c507d625a5b31a346df..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 78756 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 z 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 From 32b8a8a9f576495a1b99ff8bae1cc5e4aac15a9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Thu, 13 Oct 2022 19:53:59 +0200 Subject: [PATCH 17/29] lower the contrast of version color for photosensitivity --- assets/css/variables.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/css/variables.css b/assets/css/variables.css index 7df7abcf..092d330e 100644 --- a/assets/css/variables.css +++ b/assets/css/variables.css @@ -62,7 +62,7 @@ html[data-theme="dark"] { --package-list-item-name-color: hsl(294 40% 60%); --package-list-item-metadata-color: hsl(220 13% 91%); --package-list-item-synopsis-color: hsl(220 13% 91%); - --package-list-item-version-color: hsl(120 80% 50%); + --package-list-item-version-color: hsl(140 60% 35%); --search-bar-color: hsl(216 12% 84%); --search-bar-background-color: hsl(218 30% 25%); --search-bar-background-hover-color: hsl(218 30% 25%); From 8b7e548458e339cbf84b07511bcd3429cd847b21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Fri, 14 Oct 2022 11:23:03 +0200 Subject: [PATCH 18/29] [FLORA-246] Display package flags (#247) * [FLORA-246] Display package flags * lint * add changelog entry * Use
for flags Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- CHANGELOG.md | 1 + assets/css/app.css | 24 ++++++++++- src/Distribution/Orphans.hs | 8 ++++ src/Flora/Import/Package.hs | 10 +++-- src/Flora/Model/Release/Types.hs | 13 +++++- src/FloraWeb/Templates/Pages/Packages.hs | 51 +++++++++++++++++++----- 6 files changed, 90 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 735d340f..9099831e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * Packages are no longer kept as their own dependent (#242) * Show library dependencies only (#244) * Show licenses in package listings (#245) +* Show package flags (#246) ## v1.0.4 -- 2022-10-02 diff --git a/assets/css/app.css b/assets/css/app.css index 6a52dc23..fa4e6b57 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -193,6 +193,28 @@ div[class="bullets"] { font-weight: 500; } + .package-body-section { + font-size: 1.5rem; + line-height: 2rem; + margin-bottom: 0.75rem; + } + + .package-right-column { + width: 17em; + } + + .package-flags { + .package-flag-description { + padding-left: 0.5rem; + margin-bottom: 0.5em; + } + + .package-flag-name { + display: inline; + background-color: var(--readme-code-background-color); + } + } + .package-body-section__license { font-weight: 500; } @@ -447,7 +469,7 @@ div[class="bullets"] { /* smaller display rules */ -@media (max-width: px) { +@media (max-width: 640px) { .package-left-column { order: 1; } diff --git a/src/Distribution/Orphans.hs b/src/Distribution/Orphans.hs index 9aa576b4..f057f384 100644 --- a/src/Distribution/Orphans.hs +++ b/src/Distribution/Orphans.hs @@ -6,6 +6,7 @@ import Data.Aeson import Data.Aeson.Encoding qualified as Aeson import Data.ByteString (ByteString) import Data.ByteString.Char8 qualified as C8 +import Data.Function (on) import Data.Text qualified as Text import Data.Text.Display import Data.Text.Lazy.Builder qualified as Builder @@ -26,6 +27,7 @@ import Distribution.SPDX.License qualified as SPDX import Distribution.System (Arch, OS) import Distribution.Types.Condition import Distribution.Types.ConfVar +import Distribution.Types.Flag (PackageFlag (..)) import Distribution.Types.UnqualComponentName (UnqualComponentName, unUnqualComponentName) import Distribution.Types.Version qualified as Cabal import Distribution.Utils.Generic (fromUTF8BS) @@ -103,3 +105,9 @@ instance ToField SPDX.License where instance Display UnqualComponentName where displayBuilder = Builder.fromString . unUnqualComponentName + +instance Ord PackageFlag where + compare = compare `on` flagName + +deriving instance ToJSON PackageFlag +deriving instance FromJSON PackageFlag diff --git a/src/Flora/Import/Package.hs b/src/Flora/Import/Package.hs index 26c9d1b8..23267d14 100644 --- a/src/Flora/Import/Package.hs +++ b/src/Flora/Import/Package.hs @@ -57,6 +57,8 @@ import Optics.Core import System.Directory qualified as System import System.FilePath +import Data.Vector (Vector) +import Data.Vector qualified as Vector import Effectful.Log (Log) import Flora.Import.Categories.Tuning qualified as Tuning import Flora.Import.Types @@ -213,6 +215,7 @@ persistImportOutput (ImportOutput package categories release components) = do extractPackageDataFromCabal :: [DB, IOE] :>> es => UserId -> GenericPackageDescription -> Eff es ImportOutput extractPackageDataFromCabal userId genericDesc = do let packageDesc = genericDesc.packageDescription + let flags = Vector.fromList genericDesc.genPackageFlags let packageName = packageDesc ^. #package % #pkgName % to unPackageName % to pack % to PackageName let packageVersion = packageDesc.package.pkgVersion let namespace = chooseNamespace packageName @@ -244,6 +247,7 @@ extractPackageDataFromCabal userId genericDesc = do , maintainer = display packageDesc.maintainer , synopsis = display packageDesc.synopsis , description = display packageDesc.description + , flags = flags } let release = @@ -419,9 +423,9 @@ buildDependency package packageComponentId (Cabal.Dependency depName versionRang } in ImportDependency{package = dependencyPackage, requirement} -getRepoURL :: PackageName -> [Cabal.SourceRepo] -> [Text] -getRepoURL _ [] = [] -getRepoURL _ (repo : _) = [display $ fromMaybe mempty (repo.repoLocation)] +getRepoURL :: PackageName -> [Cabal.SourceRepo] -> Vector Text +getRepoURL _ [] = Vector.empty +getRepoURL _ (repo : _) = Vector.singleton $ display $ fromMaybe mempty (repo.repoLocation) chooseNamespace :: PackageName -> Namespace chooseNamespace name | Set.member name coreLibraries = Namespace "haskell" diff --git a/src/Flora/Model/Release/Types.hs b/src/Flora/Model/Release/Types.hs index d48e3bb8..3a6fd12b 100644 --- a/src/Flora/Model/Release/Types.hs +++ b/src/Flora/Model/Release/Types.hs @@ -26,7 +26,9 @@ import Distribution.SPDX.License qualified as SPDX import Distribution.Types.Version import GHC.Generics (Generic) +import Data.Vector (Vector) import Distribution.Orphans () +import Distribution.Types.Flag (PackageFlag) import Flora.Model.Package import Lucid qualified @@ -107,20 +109,27 @@ instance Display ImportStatus where instance FromField ImportStatus where fromField f Nothing = returnError UnexpectedNull f "" fromField _ (Just bs) | Just status <- parseImportStatus bs = pure status - fromField f (Just bs) = returnError ConversionFailed f $ unpack $ "Conversion error: Expected component to be one of " <> display @[ImportStatus] [minBound .. maxBound] <> ", but instead got " <> decodeUtf8 bs + fromField f (Just bs) = + returnError ConversionFailed f $ + unpack $ + "Conversion error: Expected component to be one of " + <> display @[ImportStatus] [minBound .. maxBound] + <> ", but instead got " + <> decodeUtf8 bs instance ToField ImportStatus where toField = Escape . encodeUtf8 . display data ReleaseMetadata = ReleaseMetadata { license :: SPDX.License - , sourceRepos :: [Text] + , sourceRepos :: Vector Text , homepage :: Maybe Text , documentation :: Text , bugTracker :: Maybe Text , maintainer :: Text , synopsis :: Text , description :: Text + , flags :: Vector PackageFlag } deriving stock (Eq, Ord, Show, Generic, Typeable) deriving anyclass (ToJSON, FromJSON) diff --git a/src/FloraWeb/Templates/Pages/Packages.hs b/src/FloraWeb/Templates/Pages/Packages.hs index 0bea134e..83a206d2 100644 --- a/src/FloraWeb/Templates/Pages/Packages.hs +++ b/src/FloraWeb/Templates/Pages/Packages.hs @@ -2,6 +2,7 @@ module FloraWeb.Templates.Pages.Packages where import Data.Foldable (fold) import Data.Text (Text, pack) +import Data.Text qualified as Text import Data.Text.Display import Data.Time (defaultTimeLocale) import Data.Time qualified as Time @@ -10,6 +11,8 @@ import Data.Vector qualified as V import Data.Vector qualified as Vector import Distribution.Pretty (pretty) import Distribution.SPDX.License qualified as SPDX +import Distribution.Types.Flag (PackageFlag (..)) +import Distribution.Types.Flag qualified as Flag import Distribution.Version import Flora.Model.Category (Category (..)) import Flora.Model.Package.Types @@ -111,12 +114,13 @@ packageBody div_ [class_ "release-readme-column grow"] $ do div_ [class_ "grid-rows-3 release-readme"] $ do displayReadme latestRelease - div_ [class_ "package-right-column md:max-w-xs"] $ do + div_ [class_ "package-right-column"] $ do ul_ [class_ "package-right-rows grid-rows-3 md:sticky md:top-28"] $ do displayInstructions packageName latestRelease displayMaintainer (metadata.maintainer) displayDependencies (namespace, packageName) numberOfDependencies dependencies displayDependents (namespace, packageName) numberOfDependents dependents + displayPackageFlags metadata.flags displayReadme :: Release -> FloraHTML displayReadme release = @@ -152,9 +156,10 @@ displayLinks namespace packageName release meta@ReleaseMetadata{..} = do li_ [class_ "package-link"] $ displaySourceRepos sourceRepos li_ [class_ "package-link"] $ displayChangelog namespace packageName release.version release.changelog -displaySourceRepos :: [Text] -> FloraHTML -displaySourceRepos [] = toHtml @Text "No source repository" -displaySourceRepos x = a_ [href_ (head x)] "Source repository" +displaySourceRepos :: Vector Text -> FloraHTML +displaySourceRepos x + | Vector.null x = toHtml @Text "No source repository" + | otherwise = a_ [href_ (Vector.head x)] "Source repository" displayChangelog :: Namespace -> PackageName -> Version -> Maybe TextHtml -> FloraHTML displayChangelog _ _ _ Nothing = toHtml @Text "" @@ -192,7 +197,7 @@ displayDependencies -> FloraHTML displayDependencies (namespace, packageName) numberOfDependencies dependencies = do li_ [class_ "mb-5"] $ do - h3_ [class_ "lg:text-2xl package-body-section mb-3"] (toHtml $ "Dependencies (" <> display numberOfDependencies <> ")") + h3_ [class_ "package-body-section"] (toHtml $ "Dependencies (" <> display numberOfDependencies <> ")") ul_ [class_ "dependencies grid-cols-3"] $ do let deps = foldMap renderDependency dependencies let numberOfShownDependencies = fromIntegral @Int @Word (Vector.length dependencies) @@ -208,7 +213,7 @@ showAll (namespace, packageName, target) = do displayInstructions :: PackageName -> Release -> FloraHTML displayInstructions packageName latestRelease = do li_ [class_ "mb-5"] $ do - h3_ [class_ "lg:text-2xl package-body-section mb-3"] "Installation" + h3_ [class_ "package-body-section"] "Installation" div_ [class_ "items-top"] $ do div_ [class_ "space-y-2"] $ do label_ [for_ "install-string", class_ "font-light"] "In your cabal file:" @@ -223,7 +228,7 @@ displayInstructions packageName latestRelease = do displayMaintainer :: Text -> FloraHTML displayMaintainer maintainerInfo = do li_ [class_ "mb-5"] $ do - h3_ [class_ "lg:text-2xl package-body-section mb-3"] "Maintainer" + h3_ [class_ "package-body-section"] "Maintainer" p_ [class_ "maintainer-info"] (toHtml maintainerInfo) displayDependents @@ -233,7 +238,7 @@ displayDependents -> FloraHTML displayDependents (namespace, packageName) numberOfDependents dependents = do li_ [class_ "mb-5 dependents"] $ do - h3_ [class_ "lg:text-2xl package-body-section dependents mb-3"] (toHtml $ "Dependents (" <> display numberOfDependents <> ")") + h3_ [class_ "package-body-section"] (toHtml $ "Dependents (" <> display numberOfDependents <> ")") if Vector.null dependents then "" else @@ -270,10 +275,34 @@ getHomepage ReleaseMetadata{..} = case homepage of Just page -> page Nothing -> - case sourceRepos of - [] -> "⚠ No homepage provided" - x -> head x + if Vector.null sourceRepos + then "⚠ No homepage provided" + else Vector.head sourceRepos +displayPackageFlags :: Vector PackageFlag -> FloraHTML +displayPackageFlags packageFlags = + if Vector.null packageFlags + then do + mempty + else do + h3_ [class_ "package-body-section"] "Package Flags" + ul_ [class_ "package-flags"] $ + forM_ packageFlags displayPackageFlag + +displayPackageFlag :: PackageFlag -> FloraHTML +displayPackageFlag MkPackageFlag{flagName, flagDescription, flagDefault} = do + details_ [] $ do + summary_ [] $ do + pre_ [class_ "package-flag-name"] (toHtml $ Text.pack (Flag.unFlagName flagName)) + toHtmlRaw @Text " " + defaultMarker flagDefault + p_ [class_ "package-flag-description"] $ toHtml flagDescription + +defaultMarker :: Bool -> FloraHTML +defaultMarker True = em_ "(on by default)" +defaultMarker False = em_ "(off by default)" + +--- intercalateVec :: a -> Vector a -> Vector a intercalateVec sep vector = if V.null vector From f165f75589dad82a8c7d9f0ab384a4b0e1de924b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Thu, 13 Oct 2022 22:48:05 +0200 Subject: [PATCH 19/29] Store flags when importing the index --- src/Distribution/Orphans.hs | 2 ++ src/Flora/Import/Package.hs | 2 ++ src/Flora/Model/Release/Types.hs | 2 ++ src/FloraWeb/Templates/Pages/Packages.hs | 2 +- 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Distribution/Orphans.hs b/src/Distribution/Orphans.hs index f057f384..9a3af2c2 100644 --- a/src/Distribution/Orphans.hs +++ b/src/Distribution/Orphans.hs @@ -33,7 +33,9 @@ import Distribution.Types.Version qualified as Cabal import Distribution.Utils.Generic (fromUTF8BS) import Distribution.Utils.ShortText import Distribution.Version (Version, VersionRange) +import Distribution.Types.Flag (PackageFlag(..)) import Servant (FromHttpApiData (..), ToHttpApiData (..)) +import Data.Function (on) deriving anyclass instance ToJSON ConfVar deriving anyclass instance FromJSON ConfVar diff --git a/src/Flora/Import/Package.hs b/src/Flora/Import/Package.hs index 23267d14..a26a78cf 100644 --- a/src/Flora/Import/Package.hs +++ b/src/Flora/Import/Package.hs @@ -78,6 +78,8 @@ import Flora.Model.Requirement ) import Flora.Model.User import GHC.Stack (HasCallStack) +import qualified Data.Vector as Vector +import Data.Vector (Vector) {-| This tuple represents the package that depends on any associated dependency/requirement. It is used in the recursive loading of Cabal files diff --git a/src/Flora/Model/Release/Types.hs b/src/Flora/Model/Release/Types.hs index 3a6fd12b..3235dd1b 100644 --- a/src/Flora/Model/Release/Types.hs +++ b/src/Flora/Model/Release/Types.hs @@ -31,6 +31,8 @@ import Distribution.Orphans () import Distribution.Types.Flag (PackageFlag) import Flora.Model.Package import Lucid qualified +import Data.Vector (Vector) +import Distribution.Types.Flag (PackageFlag) newtype ReleaseId = ReleaseId {getReleaseId :: UUID} deriving diff --git a/src/FloraWeb/Templates/Pages/Packages.hs b/src/FloraWeb/Templates/Pages/Packages.hs index 83a206d2..d942abb7 100644 --- a/src/FloraWeb/Templates/Pages/Packages.hs +++ b/src/FloraWeb/Templates/Pages/Packages.hs @@ -14,7 +14,7 @@ import Distribution.SPDX.License qualified as SPDX import Distribution.Types.Flag (PackageFlag (..)) import Distribution.Types.Flag qualified as Flag import Distribution.Version -import Flora.Model.Category (Category (..)) +import Flora.Model.Category.Types ( Category(..) ) import Flora.Model.Package.Types ( Namespace , Package (..) From 18eebb24f18c40658c763bc62874ed628a6abaf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Fri, 14 Oct 2022 12:10:24 +0200 Subject: [PATCH 20/29] Remove useless imports --- src/Distribution/Orphans.hs | 2 -- src/Flora/Import/Package.hs | 2 -- src/Flora/Model/Release/Types.hs | 2 -- src/FloraWeb/Templates/Pages/Packages.hs | 2 +- 4 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Distribution/Orphans.hs b/src/Distribution/Orphans.hs index 9a3af2c2..f057f384 100644 --- a/src/Distribution/Orphans.hs +++ b/src/Distribution/Orphans.hs @@ -33,9 +33,7 @@ import Distribution.Types.Version qualified as Cabal import Distribution.Utils.Generic (fromUTF8BS) import Distribution.Utils.ShortText import Distribution.Version (Version, VersionRange) -import Distribution.Types.Flag (PackageFlag(..)) import Servant (FromHttpApiData (..), ToHttpApiData (..)) -import Data.Function (on) deriving anyclass instance ToJSON ConfVar deriving anyclass instance FromJSON ConfVar diff --git a/src/Flora/Import/Package.hs b/src/Flora/Import/Package.hs index a26a78cf..23267d14 100644 --- a/src/Flora/Import/Package.hs +++ b/src/Flora/Import/Package.hs @@ -78,8 +78,6 @@ import Flora.Model.Requirement ) import Flora.Model.User import GHC.Stack (HasCallStack) -import qualified Data.Vector as Vector -import Data.Vector (Vector) {-| This tuple represents the package that depends on any associated dependency/requirement. It is used in the recursive loading of Cabal files diff --git a/src/Flora/Model/Release/Types.hs b/src/Flora/Model/Release/Types.hs index 3235dd1b..3a6fd12b 100644 --- a/src/Flora/Model/Release/Types.hs +++ b/src/Flora/Model/Release/Types.hs @@ -31,8 +31,6 @@ import Distribution.Orphans () import Distribution.Types.Flag (PackageFlag) import Flora.Model.Package import Lucid qualified -import Data.Vector (Vector) -import Distribution.Types.Flag (PackageFlag) newtype ReleaseId = ReleaseId {getReleaseId :: UUID} deriving diff --git a/src/FloraWeb/Templates/Pages/Packages.hs b/src/FloraWeb/Templates/Pages/Packages.hs index d942abb7..d5166851 100644 --- a/src/FloraWeb/Templates/Pages/Packages.hs +++ b/src/FloraWeb/Templates/Pages/Packages.hs @@ -14,7 +14,7 @@ import Distribution.SPDX.License qualified as SPDX import Distribution.Types.Flag (PackageFlag (..)) import Distribution.Types.Flag qualified as Flag import Distribution.Version -import Flora.Model.Category.Types ( Category(..) ) +import Flora.Model.Category.Types (Category (..)) import Flora.Model.Package.Types ( Namespace , Package (..) From 69d031af50db42c7168bb8af4cb68e8de8580029 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Fri, 14 Oct 2022 12:45:31 +0200 Subject: [PATCH 21/29] [FLORA-246] Add a tooltip and help message for flags --- assets/css/app.css | 42 ++++++++++++++++++++++++ src/FloraWeb/Templates/Pages/Packages.hs | 28 ++++++++++++++-- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/assets/css/app.css b/assets/css/app.css index fa4e6b57..c209cfea 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -203,6 +203,10 @@ div[class="bullets"] { width: 17em; } + .package-flags-section { + display: inline; + } + .package-flags { .package-flag-description { padding-left: 0.5rem; @@ -220,6 +224,44 @@ div[class="bullets"] { } } +.instruction-tooltip { + svg { + display: inline; + width: 1rem; + height: 1rem; + } + + position: relative; /* making the .tooltip span a container for the tooltip text */ + border-bottom: 1px dashed #000; /* little indicater to indicate it's hoverable */ +} + +.instruction-tooltip::before { + content: attr(data-text); /* here's the magic */ + position: absolute; + + /* vertically center */ + top: 50%; + transform: translateY(-50%); + + /* move to right */ + left: 100%; + margin-left: 15px; /* and add a small left margin */ + + /* basic styles */ + background: #000; + border-radius: 10px; + box-shadow: 0 1px 8px rgb(0 0 0 / 50%); + color: #fff; + padding: 10px; + text-align: center; + width: 250px; + display: none; /* hide by default */ +} + +.instruction-tooltip:hover::before { + display: block; +} + .pagination-area { display: flex; padding-bottom: 40px; diff --git a/src/FloraWeb/Templates/Pages/Packages.hs b/src/FloraWeb/Templates/Pages/Packages.hs index d5166851..f6febdf7 100644 --- a/src/FloraWeb/Templates/Pages/Packages.hs +++ b/src/FloraWeb/Templates/Pages/Packages.hs @@ -24,8 +24,9 @@ import Flora.Model.Release.Types (Release (..), ReleaseMetadata (..), TextHtml ( import FloraWeb.Links qualified as Links import FloraWeb.Templates.Types (FloraHTML) import Lucid -import Lucid.Base (relaxHtmlT) +import Lucid.Base (makeAttribute, relaxHtmlT) import Lucid.Orphans () +import Lucid.Svg (clip_rule_, d_, fill_, fill_rule_, path_, viewBox_) import Servant (ToHttpApiData (..)) import Text.PrettyPrint (Doc, hcat, render) import Text.PrettyPrint qualified as PP @@ -285,7 +286,13 @@ displayPackageFlags packageFlags = then do mempty else do - h3_ [class_ "package-body-section"] "Package Flags" + h3_ [class_ "package-body-section package-flags-section"] $ do + "Package Flags" + span_ + [ dataText_ "Use the -f option with cabal commands to enable flags" + , class_ "instruction-tooltip" + ] + usageInstructionTooltip ul_ [class_ "package-flags"] $ forM_ packageFlags displayPackageFlag @@ -303,6 +310,23 @@ defaultMarker True = em_ "(on by default)" defaultMarker False = em_ "(off by default)" --- + +usageInstructionTooltip :: FloraHTML +usageInstructionTooltip = do + svg_ [xmlns_ "http://www.w3.org/2000/svg", viewBox_ "0 0 20 20", fill_ "currentColor", class_ "w-5 h-5 tooltip"] $ + path_ + [ fill_rule_ "evenodd" + , d_ + "M18 10a8 8 0 11-16 0 8 8 0 0116 0zM8.94 6.94a.75.75 0 11-1.061-1.061\ + \ 3 3 0 112.871 5.026v.345a.75.75 0 01-1.5 0v-.5c0-.72.57-1.172 1.081-1.287A1.5 1.5 0 108.94 6.94zM10\ + \ 15a1 1 0 100-2 1 1 0 000 2z" + , clip_rule_ "evenodd" + ] + +-- | @datalist@ element +dataText_ :: Text -> Attribute +dataText_ = makeAttribute "data-text" + intercalateVec :: a -> Vector a -> Vector a intercalateVec sep vector = if V.null vector From 72303f2a7fa0a626c68c74db6c376b191e13e143 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Sat, 15 Oct 2022 00:59:34 +0200 Subject: [PATCH 22/29] [NO-ISSUE] A more fixed size for the left package column. Definitely should be using flexbox. --- assets/css/app.css | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/assets/css/app.css b/assets/css/app.css index c209cfea..6955df49 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -203,6 +203,10 @@ div[class="bullets"] { width: 17em; } + .package-left-column { + min-width: 11rem; + } + .package-flags-section { display: inline; } @@ -511,7 +515,7 @@ div[class="bullets"] { /* smaller display rules */ -@media (max-width: 640px) { +@media (max-width: 1023px) { .package-left-column { order: 1; } From fa6a7f7ff4c6d32c9e6ee4896151cb065a82a93d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Sat, 15 Oct 2022 13:34:22 +0200 Subject: [PATCH 23/29] Better handling of unknown packages --- src/FloraWeb/Server/Guards.hs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/FloraWeb/Server/Guards.hs b/src/FloraWeb/Server/Guards.hs index 03e0efb1..abdf9b76 100644 --- a/src/FloraWeb/Server/Guards.hs +++ b/src/FloraWeb/Server/Guards.hs @@ -24,7 +24,10 @@ guardThatPackageExists namespace packageName = do result <- Query.getPackageByNamespaceAndName namespace packageName case result of Nothing -> renderError templateEnv notFound404 - Just package -> pure package + Just package -> + case package.status of + FullyImportedPackage -> pure package + UnknownPackage -> renderError templateEnv notFound404 guardThatReleaseExists :: Namespace -> PackageName -> Version -> FloraPage Release guardThatReleaseExists namespace packageName version = do From 70c87aa56d680942629abfd45e22c61f8d7afd42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Sat, 15 Oct 2022 16:31:45 +0200 Subject: [PATCH 24/29] Rectify the width of the package right column --- assets/css/app.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/assets/css/app.css b/assets/css/app.css index 6955df49..ad1c1f64 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -200,7 +200,8 @@ div[class="bullets"] { } .package-right-column { - width: 17em; + min-width: 13em; + max-width: 13em; } .package-left-column { From 573ffe20b25b7599c3e2ca37512b4dc8a617eafc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Mon, 17 Oct 2022 00:56:10 +0200 Subject: [PATCH 25/29] [FLORA-137] Render description to HTML when there is no README (#248) * [FLORA-137] Render description to HTML when there is no README * Fix tables * add changelog entry * fix CI --- CHANGELOG.md | 1 + assets/css/release-readme.css | 4 + flora.cabal | 6 +- src/FloraWeb/Templates/Haddock.hs | 115 ++++++++++ src/FloraWeb/Templates/Pages/Packages.hs | 14 +- test/Flora/PackageSpec.hs | 3 +- test/Flora/TestUtils.hs | 4 + test/fixtures/Cabal/filepath.cabal | 243 +++++++++++++++----- test/fixtures/Cabal/relude.cabal | 281 +++++++++++++++++++++++ 9 files changed, 610 insertions(+), 61 deletions(-) create mode 100644 src/FloraWeb/Templates/Haddock.hs create mode 100644 test/fixtures/Cabal/relude.cabal diff --git a/CHANGELOG.md b/CHANGELOG.md index 9099831e..3a66a7ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ * Show library dependencies only (#244) * Show licenses in package listings (#245) * Show package flags (#246) +* Renders release descriptions when no README is present (#248) ## v1.0.4 -- 2022-10-02 diff --git a/assets/css/release-readme.css b/assets/css/release-readme.css index e1b504cc..12400b82 100644 --- a/assets/css/release-readme.css +++ b/assets/css/release-readme.css @@ -110,4 +110,8 @@ background-color: var(--readme-code-background-color); border-radius: 6px; } + + tr { + border-bottom-width: 1px; + } } diff --git a/flora.cabal b/flora.cabal index 22fceb48..d0215d85 100644 --- a/flora.cabal +++ b/flora.cabal @@ -33,6 +33,7 @@ common common-extensions DuplicateRecordFields LambdaCase OverloadedLabels + OverloadedRecordDot OverloadedStrings PolyKinds QuasiQuotes @@ -41,7 +42,6 @@ common common-extensions TypeFamilies UndecidableInstances ViewPatterns - OverloadedRecordDot default-language: GHC2021 @@ -155,6 +155,7 @@ library FloraWeb.Templates.Admin.Packages FloraWeb.Templates.Admin.Users FloraWeb.Templates.Error + FloraWeb.Templates.Haddock FloraWeb.Templates.Packages.Changelog FloraWeb.Templates.Packages.Dependencies FloraWeb.Templates.Packages.Dependents @@ -193,7 +194,9 @@ library , effectful , effectful-core , envparse ^>=0.5 + , extra , filepath ^>=1.4 + , haddock-library ^>=1.11 , http-api-data ^>=0.4 , http-client ^>=0.7.10 , http-client-tls @@ -210,6 +213,7 @@ library , monad-control ^>=1.0 , monad-time ^>=0.4 , mtl ^>=2.2 + , network-uri , odd-jobs , optics-core ^>=0.4 , optparse-applicative ^>=0.16 diff --git a/src/FloraWeb/Templates/Haddock.hs b/src/FloraWeb/Templates/Haddock.hs new file mode 100644 index 00000000..eddbc339 --- /dev/null +++ b/src/FloraWeb/Templates/Haddock.hs @@ -0,0 +1,115 @@ +module FloraWeb.Templates.Haddock where + +import Control.Monad (forM_) +import Data.Maybe +import Data.Text (Text) +import Data.Text qualified as Text +import Data.Text.Display (display) +import Distribution.ModuleName (ModuleName) +import Distribution.Text (simpleParse) +import Documentation.Haddock.Markup qualified as Haddock +import Documentation.Haddock.Parser qualified as Haddock +import Documentation.Haddock.Types (DocMarkupH (..), Example (..), Header (..), Hyperlink (..), MetaDoc (..), ModLink (..), Picture (..), Table (..), TableCell (..), TableRow (..)) +import Flora.Model.Package (PackageName (..)) +import FloraWeb.Templates (FloraHTML) +import Lucid +import Network.URI + +renderHaddock :: PackageName -> Text -> FloraHTML +renderHaddock (PackageName package) input = do + let metaDoc = Haddock.parseParas (Just (Text.unpack package)) (Text.unpack input) + let normalisedDoc = Haddock.toRegular metaDoc._doc + Haddock.markup (htmlMarkup (const Nothing)) normalisedDoc + +htmlMarkup :: (ModuleName -> Maybe Text) -> DocMarkupH mod String FloraHTML +htmlMarkup modResolv = + Markup + { markupEmpty = mempty + , markupString = toHtml + , markupParagraph = p_ + , markupAppend = (<>) + , markupIdentifier = code_ . toHtml + , markupIdentifierUnchecked = const $ code_ [] (toHtml @Text "FIXME") -- should never happen + , markupModule = mkModLink + , markupWarning = div_ [class_ "warning"] + , markupEmphasis = em_ + , markupBold = strong_ + , markupMonospaced = code_ + , markupUnorderedList = \listItems -> ul_ [] $ forM_ listItems $ \item -> li_ item + , markupOrderedList = \listItems -> ol_ [] $ forM_ listItems $ \(_, item) -> li_ [] item + , markupDefList = \listItems -> + dl_ [] $ + forM_ + listItems + ( \(dTerm, definition) -> do + dt_ [] dTerm + dd_ [] definition + ) + , markupCodeBlock = pre_ [] + , markupHyperlink = \(Hyperlink strUrl mLabel) -> + let url = Text.pack strUrl + in a_ [href_ url] $ fromMaybe (toHtml url) mLabel + , markupAName = (`namedAnchor` "") + , markupPic = \(Picture uri mTitle) -> i_ [src_ (Text.pack uri), title_ (maybe "" Text.pack mTitle)] "" + , markupMathInline = \mathjax -> toHtml ("\\(" ++ mathjax ++ "\\)") + , markupMathDisplay = \mathjax -> toHtml ("\\[" ++ mathjax ++ "\\]") + , markupProperty = pre_ . toHtml + , markupExample = examplesToHtml + , markupHeader = \(Header l t) -> makeHeader l t + , markupTable = \(Table h r) -> makeTable h r + } + where + makeHeader :: Int -> FloraHTML -> FloraHTML + makeHeader 1 mkup = h2_ mkup + makeHeader 2 mkup = h3_ mkup + makeHeader 3 mkup = h4_ mkup + makeHeader 4 mkup = h5_ mkup + makeHeader _ mkup = h6_ mkup + + examplesToHtml :: [Example] -> FloraHTML + examplesToHtml examples = pre_ [class_ "screen"] $ forM_ examples $ \e -> exampleToHtml e + + exampleToHtml :: Example -> FloraHTML + exampleToHtml (Example expression result) = do + code_ [class_ "prompt"] ">>>" + strong_ [class_ "userinput"] $ do + code_ [] (pure $ Text.pack $ expression <> "\n") + forM_ result (\resultLine -> pure $ Text.pack resultLine) + + mkModLink :: ModLink FloraHTML -> FloraHTML + mkModLink (ModLink name _mLabel) = + case extractModInfo (Text.pack name) of + Nothing -> code_ (toHtml name) + Just html -> html + + extractModInfo :: Text -> Maybe FloraHTML + extractModInfo name = do + modname <- simpleParse (Text.unpack name) + modUrl <- modResolv modname + pure $ + span_ [class_ "module"] $ + a_ [href_ modUrl] (toHtml name) + + makeTable :: [TableRow FloraHTML] -> [TableRow FloraHTML] -> FloraHTML + makeTable headers cells = table_ $ do + thead_ $ do + forM_ headers $ \(TableRow cs) -> tr_ [] $ forM_ cs $ \cell -> makeHeaderCell cell + forM_ cells $ \(TableRow cs) -> tr_ [] $ forM_ cs $ \cell -> makeDataCell cell + + makeHeaderCell :: TableCell FloraHTML -> FloraHTML + makeHeaderCell (TableCell colSpan rowSpan content) = + th_ attrs content + where + attrs = i <> j + i = [colspan_ (display colSpan) | colSpan /= 1] + j = [rowspan_ (display rowSpan) | rowSpan /= 1] + + makeDataCell :: TableCell FloraHTML -> FloraHTML + makeDataCell (TableCell colSpan rowSpan content) = + td_ [colspan_ (display colSpan), rowspan_ (display rowSpan)] content + +namedAnchor :: String -> FloraHTML -> FloraHTML +namedAnchor n = a_ [name_ (Text.pack $ escapeStr n)] + +escapeStr :: String -> String +escapeStr = escapeURIString isUnreserved diff --git a/src/FloraWeb/Templates/Pages/Packages.hs b/src/FloraWeb/Templates/Pages/Packages.hs index f6febdf7..a98efe99 100644 --- a/src/FloraWeb/Templates/Pages/Packages.hs +++ b/src/FloraWeb/Templates/Pages/Packages.hs @@ -18,10 +18,11 @@ import Flora.Model.Category.Types (Category (..)) import Flora.Model.Package.Types ( Namespace , Package (..) - , PackageName + , PackageName (..) ) import Flora.Model.Release.Types (Release (..), ReleaseMetadata (..), TextHtml (..)) import FloraWeb.Links qualified as Links +import FloraWeb.Templates.Haddock (renderHaddock) import FloraWeb.Templates.Types (FloraHTML) import Lucid import Lucid.Base (makeAttribute, relaxHtmlT) @@ -114,7 +115,7 @@ packageBody displayVersions namespace packageName packageReleases numberOfReleases div_ [class_ "release-readme-column grow"] $ do div_ [class_ "grid-rows-3 release-readme"] $ do - displayReadme latestRelease + displayReadme packageName latestRelease div_ [class_ "package-right-column"] $ do ul_ [class_ "package-right-rows grid-rows-3 md:sticky md:top-28"] $ do displayInstructions packageName latestRelease @@ -123,12 +124,15 @@ packageBody displayDependents (namespace, packageName) numberOfDependents dependents displayPackageFlags metadata.flags -displayReadme :: Release -> FloraHTML -displayReadme release = +displayReadme :: PackageName -> Release -> FloraHTML +displayReadme packageName release = case readme release of - Nothing -> toHtml @Text "no readme available" + Nothing -> renderDescription packageName release.metadata.description Just (MkTextHtml readme) -> relaxHtmlT readme +renderDescription :: PackageName -> Text -> FloraHTML +renderDescription packageName input = renderHaddock packageName input + displayReleaseVersion :: Version -> FloraHTML displayReleaseVersion version = toHtml version diff --git a/test/Flora/PackageSpec.hs b/test/Flora/PackageSpec.hs index 16730f12..42ec2913 100644 --- a/test/Flora/PackageSpec.hs +++ b/test/Flora/PackageSpec.hs @@ -79,7 +79,7 @@ testFetchGHCPrimDependents = do testThatBaseisInPreludeCategory :: TestEff () testThatBaseisInPreludeCategory = do result <- Query.getPackagesFromCategorySlug "prelude" - assertEqual (Set.fromList [PackageName "base"]) (Set.fromList $ V.toList $ fmap (view #name) result) + assertBool $ Set.member (PackageName "base") (Set.fromList $ V.toList $ fmap (view #name) result) testThatSemigroupsIsInMathematicsAndDataStructures :: TestEff () testThatSemigroupsIsInMathematicsAndDataStructures = do @@ -109,6 +109,7 @@ testNoSelfDependent = do [ PackageName "flora" , PackageName "hashable" , PackageName "jose" + , PackageName "relude" , PackageName "semigroups" , PackageName "xml" ] diff --git a/test/Flora/TestUtils.hs b/test/Flora/TestUtils.hs index 053ccf1c..bef4e2b4 100644 --- a/test/Flora/TestUtils.hs +++ b/test/Flora/TestUtils.hs @@ -8,6 +8,7 @@ module Flora.TestUtils , testThese -- * Assertion functions + , assertBool , assertEqual , assertFailure , assertRight @@ -143,6 +144,9 @@ testThese groupName tests = fmap (Test.testGroup groupName) newTests newTests :: TestEff [TestTree] newTests = sequenceA tests +assertBool :: Bool -> TestEff () +assertBool boolean = liftIO $ Test.assertBool "" boolean + -- | 'assertEqual' @expected@ @actual@ assertEqual :: (Eq a, Show a) => a -> a -> TestEff () assertEqual expected actual = liftIO $ Test.assertEqual "" expected actual diff --git a/test/fixtures/Cabal/filepath.cabal b/test/fixtures/Cabal/filepath.cabal index 9a834873..892cb77e 100644 --- a/test/fixtures/Cabal/filepath.cabal +++ b/test/fixtures/Cabal/filepath.cabal @@ -1,68 +1,203 @@ -cabal-version: 1.18 -name: filepath -version: 1.4.2.2 +cabal-version: 2.2 +name: filepath +version: 1.4.100.0 + -- NOTE: Don't forget to update ./changelog.md -license: BSD3 -license-file: LICENSE -author: Neil Mitchell -maintainer: Julian Ospald -copyright: Neil Mitchell 2005-2020 -bug-reports: https://github.com/haskell/filepath/issues -homepage: https://github.com/haskell/filepath#readme -category: System -build-type: Simple -synopsis: Library for manipulating FilePaths in a cross platform way. -tested-with: GHC==9.2.1, GHC==9.0.1, GHC==8.10.7, GHC==8.8.4, GHC==8.6.5, GHC==8.4.4, GHC==8.2.2, GHC==8.0.2 +license: BSD-3-Clause +license-file: LICENSE +author: Neil Mitchell +maintainer: Julian Ospald +copyright: Neil Mitchell 2005-2020, Julain Ospald 2021-2022 +bug-reports: https://gitlab.haskell.org/haskell/filepath/-/issues +homepage: + https://gitlab.haskell.org/haskell/filepath/-/blob/master/README.md + +category: System +build-type: Simple +synopsis: Library for manipulating FilePaths in a cross platform way. +tested-with: + GHC ==8.0.2 + || ==8.2.2 + || ==8.4.4 + || ==8.6.5 + || ==8.8.4 + || ==8.10.7 + || ==9.0.2 + || ==9.2.3 + description: - This package provides functionality for manipulating @FilePath@ values, and is shipped with both and the . It provides three modules: - . - * "System.FilePath.Posix" manipulates POSIX\/Linux style @FilePath@ values (with @\/@ as the path separator). - . - * "System.FilePath.Windows" manipulates Windows style @FilePath@ values (with either @\\@ or @\/@ as the path separator, and deals with drives). - . - * "System.FilePath" is an alias for the module appropriate to your platform. - . - All three modules provide the same API, and the same documentation (calling out differences in the different variants). + This package provides functionality for manipulating @FilePath@ values, and is shipped with . It provides two variants for filepaths: + . + 1. legacy filepaths: @type FilePath = String@ + . + 2. operating system abstracted filepaths (@OsPath@): internally unpinned @ShortByteString@ (platform-dependent encoding) + . + It is recommended to use @OsPath@ when possible, because it is more correct. + . + For each variant there are three main modules: + . + * "System.FilePath.Posix" / "System.OsPath.Posix" manipulates POSIX\/Linux style @FilePath@ values (with @\/@ as the path separator). + . + * "System.FilePath.Windows" / "System.OsPath.Windows" manipulates Windows style @FilePath@ values (with either @\\@ or @\/@ as the path separator, and deals with drives). + . + * "System.FilePath" / "System.OsPath" for dealing with current platform-specific filepaths + . + "System.OsString" is like "System.OsPath", but more general purpose. Refer to the documentation of + those modules for more information. extra-source-files: - System/FilePath/Internal.hs - Makefile + Generate.hs + Makefile + System/FilePath/Internal.hs + System/OsPath/Common.hs + System/OsString/Common.hs + tests/bytestring-tests/Properties/Common.hs + extra-doc-files: - README.md - HACKING.md - changelog.md + changelog.md + HACKING.md + README.md + +flag cpphs + description: Use cpphs (fixes haddock source links) + default: False + manual: True source-repository head - type: git - location: https://github.com/haskell/filepath.git + type: git + location: https://gitlab.haskell.org/haskell/filepath library - default-language: Haskell2010 - other-extensions: - CPP - PatternGuards - if impl(GHC >= 7.2) - other-extensions: Safe + exposed-modules: + System.FilePath + System.FilePath.Posix + System.FilePath.Windows + System.OsPath + System.OsPath.Data.ByteString.Short + System.OsPath.Data.ByteString.Short.Internal + System.OsPath.Data.ByteString.Short.Word16 + System.OsPath.Encoding + System.OsPath.Encoding.Internal + System.OsPath.Internal + System.OsPath.Posix + System.OsPath.Posix.Internal + System.OsPath.Types + System.OsPath.Windows + System.OsPath.Windows.Internal + System.OsString + System.OsString.Internal + System.OsString.Internal.Types + System.OsString.Posix + System.OsString.Windows - exposed-modules: - System.FilePath - System.FilePath.Posix - System.FilePath.Windows + other-extensions: + CPP + PatternGuards - build-depends: - base >= 4.9 && < 4.17 + if impl(ghc >=7.2) + other-extensions: Safe - ghc-options: -Wall + default-language: Haskell2010 + build-depends: + , base >=4.9 && <4.18 + , bytestring >=0.11.3.0 + , deepseq + , exceptions + , template-haskell + + ghc-options: -Wall + + if flag(cpphs) + ghc-options: -pgmPcpphs -optP--cpp + build-tool-depends: cpphs:cpphs -any test-suite filepath-tests - type: exitcode-stdio-1.0 - default-language: Haskell2010 - main-is: Test.hs - hs-source-dirs: tests - other-modules: - TestGen - TestUtil - build-depends: - filepath, - base, - QuickCheck >= 2.7 && < 2.15 + type: exitcode-stdio-1.0 + main-is: Test.hs + hs-source-dirs: tests tests/filepath-tests + other-modules: + TestGen + TestUtil + + build-depends: + , base + , bytestring >=0.11.3.0 + , filepath + , QuickCheck >=2.7 && <2.15 + + default-language: Haskell2010 + ghc-options: -Wall + +test-suite filepath-equivalent-tests + default-language: Haskell2010 + ghc-options: -Wall + type: exitcode-stdio-1.0 + main-is: TestEquiv.hs + hs-source-dirs: tests tests/filepath-equivalent-tests + other-modules: + Legacy.System.FilePath + Legacy.System.FilePath.Posix + Legacy.System.FilePath.Windows + TestUtil + + build-depends: + , base + , bytestring >=0.11.3.0 + , filepath + , QuickCheck >=2.7 && <2.15 + +test-suite bytestring-tests + default-language: Haskell2010 + ghc-options: -Wall + type: exitcode-stdio-1.0 + main-is: Main.hs + hs-source-dirs: tests tests/bytestring-tests + other-modules: + Properties.ShortByteString + Properties.ShortByteString.Word16 + TestUtil + + build-depends: + , base + , bytestring >=0.11.3.0 + , filepath + , QuickCheck >=2.7 && <2.15 + +test-suite abstract-filepath + default-language: Haskell2010 + ghc-options: -Wall + type: exitcode-stdio-1.0 + main-is: Test.hs + hs-source-dirs: tests tests/abstract-filepath + other-modules: + Arbitrary + EncodingSpec + OsPathSpec + TestUtil + + build-depends: + , base + , bytestring >=0.11.3.0 + , checkers ^>=0.5.6 + , deepseq + , filepath + , QuickCheck >=2.7 && <2.15 + +benchmark bench-filepath + default-language: Haskell2010 + ghc-options: -Wall + type: exitcode-stdio-1.0 + main-is: BenchFilePath.hs + hs-source-dirs: bench + other-modules: TastyBench + build-depends: + , base + , bytestring >=0.11.3.0 + , deepseq + , filepath + + if impl(ghc >=8.10) + ghc-options: "-with-rtsopts=-A32m --nonmoving-gc" + + else + ghc-options: -with-rtsopts=-A32m diff --git a/test/fixtures/Cabal/relude.cabal b/test/fixtures/Cabal/relude.cabal new file mode 100644 index 00000000..d154f58e --- /dev/null +++ b/test/fixtures/Cabal/relude.cabal @@ -0,0 +1,281 @@ +cabal-version: 3.0 +name: relude +version: 1.1.0.0 +synopsis: Safe, performant, user-friendly and lightweight Haskell Standard Library +description: + @__relude__@ is an alternative prelude library. If you find the default + @Prelude@ unsatisfying, despite its advantages, consider using @relude@ + instead. + + == Relude goals and design principles + * __Productivity.__ You can be more productive with a "non-standard" standard + library, and @relude@ helps you with writing safer and more + efficient code faster. + + * __Total programming__. Usage of [/partial functions/](https://www.reddit.com/r/haskell/comments/5n51u3/why_are_partial_functions_as_in_head_tail_bad/) + can lead to unexpected bugs and runtime exceptions in pure + code. The types of partial functions lie about their behaviour. And + even if it is not always possible to rely only on total functions, + @relude@ strives to encourage best-practices and reduce the + chances of introducing a bug. + + +---------------------------------+--------------------------------------------+ + | __Partial__ | __Total__ | + +=================================+============================================+ + | @head :: [a] -> a@ | @head :: NonEmpty a -> a@ | + +---------------------------------+--------------------------------------------+ + | @tail :: [a] -> [a]@ | @tail :: NonEmpty a -> [a]@ | + +---------------------------------+--------------------------------------------+ + | @read :: Read a => String -> a@ | @readMaybe :: Read a => String -> Maybe a@ | + +---------------------------------+--------------------------------------------+ + | @fromJust :: Maybe a -> a@ | @fromMaybe :: a -> Maybe a -> a@ | + +---------------------------------+--------------------------------------------+ + + * __Type-safety__. We use the /"make invalid states unrepresentable"/ motto as one + of our guiding principles. If it is possible, we express this concept through the + types. + + /Example:/ @ whenNotNull :: Applicative f => [a] -> (NonEmpty a -> f ()) -> f () @ + + * __Performance.__ We prefer @Text@ over @[String](https://www.reddit.com/r/haskell/comments/29jw0s/whats_wrong_with_string/)@, + use space-leaks-free functions (e.g. our custom performant @sum@ and @product@), + introduce @\{\-\# INLINE \#\-\}@ and @\{\-\# SPECIALIZE \#\-\}@ pragmas where + appropriate, and make efficient container types + (e.g. @Map@, @HashMap@, @Set@) more accessible. + + * __Minimalism__ (low number of dependencies). We do not force users of + @relude@ to stick to any specific lens or text formatting or logging + library. Where possible, @relude@ depends only on boot libraries. + The [Dependency graph](https://raw.githubusercontent.com/kowainik/relude/main/relude-dependency-graph.png) + of @relude@ can give you a clearer picture. + + * __Convenience__. Despite minimalism, we want to bring commonly used + types and functions into scope, and make available functions easier + to use. Some examples of conveniences: + + 1. No need to add @containers@, @unordered-containers@, @text@ + and @bytestring@ to dependencies in your @.cabal@ file to + use the main API of these libraries + 2. No need to import types like @NonEmpty@, @Text@, @Set@, @Reader[T]@, @MVar@, @STM@ + 3. Functions like @liftIO@, @fromMaybe@, @sortWith@ are available by default as well + 4. @IO@ actions are lifted to @MonadIO@ + + * __Excellent documentation.__ + + 1. Tutorial + 2. Migration guide from @Prelude@ + 3. Haddock for every function with examples tested by + [doctest](http://hackage.haskell.org/package/doctest). + 4. Documentation regarding [internal module structure](http://hackage.haskell.org/package/relude/docs/Relude.html) + 5. @relude@-specific [HLint](http://hackage.haskell.org/package/hlint) rules: @[.hlint.yaml](https://github.com/kowainik/relude/blob/main/.hlint.yaml)@ + + * __User-friendliness.__ Anyone should be able to quickly migrate to @relude@. Only + some basic familiarity with the common libraries like @text@ and @containers@ + should be enough (but not necessary). + + * __Exploration.__ We have space to experiment with new ideas and proposals + without introducing breaking changes. @relude@ uses the approach with + @Extra.*@ modules which are not exported by default. The chosen approach makes it quite + easy for us to provide new functionality without breaking anything and let + the users decide to use it or not. + +homepage: https://github.com/kowainik/relude +bug-reports: https://github.com/kowainik/relude/issues +license: MIT +license-file: LICENSE +author: Dmitrii Kovanikov, Veronika Romashkina, Stephen Diehl, Serokell +maintainer: Kowainik +copyright: 2016 Stephen Diehl, 2016-2018 Serokell, 2018-2021 Kowainik +category: Prelude +stability: stable +build-type: Simple +extra-doc-files: CHANGELOG.md + README.md +tested-with: GHC == 8.2.2 + GHC == 8.4.4 + GHC == 8.6.5 + GHC == 8.8.4 + GHC == 8.10.7 + GHC == 9.0.2 + GHC == 9.2.2 + + +source-repository head + type: git + location: git@github.com:kowainik/relude.git + +common common-options + build-depends: base >= 4.10 && < 4.17 + + ghc-options: -Wall + -Wcompat + -Widentities + -Wincomplete-uni-patterns + -Wincomplete-record-updates + -fwarn-implicit-prelude + -Wredundant-constraints + -fhide-source-paths + if impl(ghc >= 8.4) + ghc-options: -Wmissing-export-lists + -Wpartial-fields + if impl(ghc >= 8.8) + ghc-options: -Wmissing-deriving-strategies + if impl(ghc >= 8.10) + ghc-options: -Wunused-packages + if impl(ghc >= 9.0) + ghc-options: -Winvalid-haddock + if impl(ghc >= 9.2) + ghc-options: -Wredundant-bang-patterns + -Woperator-whitespace + + + default-language: Haskell2010 + default-extensions: InstanceSigs + NoImplicitPrelude + OverloadedStrings + ScopedTypeVariables + TypeApplications + +library + import: common-options + hs-source-dirs: src + exposed-modules: + Relude + Relude.Applicative + Relude.Base + Relude.Bool + Relude.Bool.Guard + Relude.Bool.Reexport + Relude.Container + Relude.Container.One + Relude.Container.Reexport + Relude.Debug + Relude.DeepSeq + Relude.Enum + Relude.Exception + Relude.File + Relude.Foldable + Relude.Foldable.Fold + Relude.Foldable.Reexport + Relude.Function + Relude.Functor + Relude.Functor.Fmap + Relude.Functor.Reexport + Relude.Lifted + Relude.Lifted.Concurrent + Relude.Lifted.Exit + Relude.Lifted.File + Relude.Lifted.IORef + Relude.Lifted.Terminal + Relude.Lifted.Handle + Relude.Lifted.Env + Relude.List + Relude.List.NonEmpty + Relude.List.Reexport + Relude.Monad + Relude.Monad.Either + Relude.Monad.Maybe + Relude.Monad.Reexport + Relude.Monad.Trans + Relude.Monoid + Relude.Nub + Relude.Numeric + Relude.Print + Relude.String + Relude.String.Conversion + Relude.String.Reexport + + -- not exported by default + Relude.Extra + Relude.Extra.Bifunctor + Relude.Extra.CallStack + Relude.Extra.Enum + Relude.Extra.Foldable + Relude.Extra.Foldable1 + Relude.Extra.Group + Relude.Extra.Lens + Relude.Extra.Map + Relude.Extra.Newtype + Relude.Extra.Tuple + Relude.Extra.Type + Relude.Unsafe + + reexported-modules: + -- containers + , Data.IntMap.Lazy + , Data.IntMap.Strict + , Data.IntSet + , Data.Map.Lazy + , Data.Map.Strict + , Data.Set + , Data.Sequence + , Data.Tree + -- unordered-containers + , Data.HashMap.Lazy + , Data.HashMap.Strict + , Data.HashSet + -- text + , Data.Text + , Data.Text.IO + , Data.Text.Lazy + , Data.Text.Lazy.IO + , Data.Text.Read + -- bytestring + , Data.ByteString + , Data.ByteString.Builder + , Data.ByteString.Lazy + , Data.ByteString.Short + + + build-depends: bytestring >= 0.10 && < 0.12 + , containers >= 0.5.10 && < 0.7 + , deepseq ^>= 1.4 + , ghc-prim >= 0.4.0.0 && < 0.9 + , hashable >= 1.2 && < 1.5 + , mtl ^>= 2.2 + , stm >= 2.4 && < 2.6 + , text >= 1.2 && < 2.1 + , transformers >= 0.5 && < 0.7 + , unordered-containers >= 0.2.7 && < 0.3 + + +test-suite relude-test + import: common-options + type: exitcode-stdio-1.0 + hs-source-dirs: test + main-is: Spec.hs + + other-modules: Test.Relude.Gen + Test.Relude.Container.One + Test.Relude.Property + build-depends: relude + , bytestring + , containers + , text + , hedgehog >= 1.0 && < 1.2 + + ghc-options: -threaded + +test-suite relude-doctest + import: common-options + type: exitcode-stdio-1.0 + hs-source-dirs: test + main-is: Doctest.hs + + build-depends: relude + , doctest ^>= 0.20 + , Glob + + ghc-options: -threaded + +benchmark relude-benchmark + import: common-options + type: exitcode-stdio-1.0 + hs-source-dirs: benchmark + main-is: Main.hs + + build-depends: relude + , tasty-bench + , unordered-containers + + ghc-options: -rtsopts From 4b015d0c38abd85d6bf3c50fcaa9b7ae4eb9774f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Mon, 17 Oct 2022 15:51:02 +0200 Subject: [PATCH 26/29] [NO-ISSUE] Exclude `ConnectionClosedByPeer` exception from reporting --- src/FloraWeb/Server/Tracing.hs | 52 +++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/src/FloraWeb/Server/Tracing.hs b/src/FloraWeb/Server/Tracing.hs index 249940af..cd3a5788 100644 --- a/src/FloraWeb/Server/Tracing.hs +++ b/src/FloraWeb/Server/Tracing.hs @@ -1,39 +1,57 @@ module FloraWeb.Server.Tracing where -import Control.Exception (SomeException, throw) +import Control.Exception (AsyncException (..), Exception (..), SomeException, throw) +import Control.Monad.IO.Class import Data.Aeson ((.=)) import Data.Aeson qualified as Aeson import Data.ByteString.Char8 (unpack) import Data.Text.Display (display) import Flora.Environment +import GHC.IO.Exception (IOErrorType (..)) import Log (LogLevel (..), Logger, logAttention, runLogT) import Network.Wai import Network.Wai.Handler.Warp +import System.IO.Error (ioeGetErrorType) import System.Log.Raven (initRaven, register, silentFallback) import System.Log.Raven.Transport.HttpConduit (sendRecord) import System.Log.Raven.Types (SentryLevel (Error), SentryRecord (..)) onException :: Logger -> DeploymentEnv -> LoggingEnv -> Maybe Request -> SomeException -> IO () -onException logger environment tracingEnv mRequest exception = +onException logger environment tracingEnv mRequest exception = Log.runLogT "flora" logger LogAttention $ do case tracingEnv.sentryDSN of - Nothing -> Log.runLogT "flora" logger LogAttention $ do + Nothing -> do logAttention "Unhandled exception" $ Aeson.object ["exception" .= display (show exception)] throw exception - Just sentryDSN -> do - sentryService <- - initRaven - sentryDSN - (\defaultRecord -> defaultRecord{srEnvironment = Just $ show environment}) - sendRecord - silentFallback - register - sentryService - "flora-logger" - Error - (formatMessage mRequest exception) - (recordUpdate mRequest exception) - defaultOnException mRequest exception + Just sentryDSN -> + if shouldDisplayException exception + then do + sentryService <- + liftIO $ + initRaven + sentryDSN + (\defaultRecord -> defaultRecord{srEnvironment = Just $ show environment}) + sendRecord + silentFallback + liftIO $ + register + sentryService + "flora-logger" + Error + (formatMessage mRequest exception) + (recordUpdate mRequest exception) + liftIO $ defaultOnException mRequest exception + else liftIO $ defaultOnException mRequest exception + +shouldDisplayException :: SomeException -> Bool +shouldDisplayException exception + | Just ThreadKilled <- fromException exception = False + | Just (_ :: InvalidRequest) <- fromException exception = False + | Just (ioeGetErrorType -> et) <- fromException exception + , et == ResourceVanished || et == InvalidArgument = + False + | Just ConnectionClosedByPeer <- fromException exception = False + | otherwise = True formatMessage :: Maybe Request -> SomeException -> String formatMessage Nothing exception = "Exception before request could be parsed: " ++ show exception From c8a702fb8c77c9af69d1faa19f86c14299a2fb17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Sat, 22 Oct 2022 19:06:57 +0200 Subject: [PATCH 27/29] [NO-ISSUE] Fix faulty release migration --- migrations/20211106123053_create_releases.sql | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/migrations/20211106123053_create_releases.sql b/migrations/20211106123053_create_releases.sql index e61af51c..c6cca2db 100644 --- a/migrations/20211106123053_create_releases.sql +++ b/migrations/20211106123053_create_releases.sql @@ -16,15 +16,14 @@ create table if not exists releases ( changelog_status import_status, constraint consistent_readme_status check ( - ((readme_status = 'imported' or readme_status = 'inexistent') - and readme is not null) - or (readme_status = 'not-imported' and readme is null) + (readme_status = 'imported' and readme is not null) + or ((readme_status = 'not-imported' or readme_status = 'inexistent') and readme is null) ), constraint consistent_changelog_status check ( - ((changelog_status = 'imported' or changelog_status = 'inexistent') + ((changelog_status = 'imported') and changelog is not null) - or (changelog_status = 'not-imported' and changelog is null) + or ((changelog_status = 'not-imported' or changelog_status = 'inexistent') and changelog is null) ) ); From 7c057fccb2b473c9ca76836f30c09fba732be60c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Sat, 22 Oct 2022 19:29:48 +0200 Subject: [PATCH 28/29] [NO-ISSUE] Put some more space before a list item in READMEs --- assets/css/release-readme.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/assets/css/release-readme.css b/assets/css/release-readme.css index 12400b82..dc192dc4 100644 --- a/assets/css/release-readme.css +++ b/assets/css/release-readme.css @@ -63,6 +63,7 @@ padding-left: 1em; li { + margin-top: 1rem; overflow-wrap: break-word; list-style-type: decimal; list-style-position: outside; @@ -78,6 +79,7 @@ padding-left: 1em; li { + margin-top: 1rem; overflow-wrap: break-word; list-style-type: disc; list-style-position: outside; From 0fdda743c165536092cde1e65e91404c6de56756 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Sat, 22 Oct 2022 20:00:03 +0200 Subject: [PATCH 29/29] [NO-ISSUE] Prepare release 1.0.5 --- CHANGELOG.md | 2 +- cabal.project.freeze | 2 ++ flora.cabal | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a66a7ae..efa748de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # CHANGELOG -## v1.0.5 -- XXXX-XX-XX +## v1.0.5 -- 2022-10-22 * Reorder the package page columns in mobile view (#233) * Enable the use of markdown extensions in package READMEs (#236) diff --git a/cabal.project.freeze b/cabal.project.freeze index 86aa7ad3..638da6d9 100644 --- a/cabal.project.freeze +++ b/cabal.project.freeze @@ -123,6 +123,7 @@ constraints: any.Cabal ==3.6.3.0, any.envparse ==0.5.0, any.erf ==2.0.0.0, any.exceptions ==0.10.4, + any.extra ==1.7.12, any.fast-logger ==3.1.1, any.file-embed ==0.0.15.0, any.filepath ==1.4.2.2, @@ -142,6 +143,7 @@ constraints: any.Cabal ==3.6.3.0, any.ghc-heap ==9.2.4, any.ghc-prim ==0.8.0, any.ghci ==9.2.4, + any.haddock-library ==1.11.0, any.happy ==1.20.0, any.hashable ==1.4.1.0, hashable +containers +integer-gmp -random-initial-seed, diff --git a/flora.cabal b/flora.cabal index d0215d85..a9dc0a00 100644 --- a/flora.cabal +++ b/flora.cabal @@ -1,6 +1,6 @@ cabal-version: 3.0 name: flora -version: 1.0.4 +version: 1.0.5 homepage: https://github.com/flora-pm/flora-server/#readme bug-reports: https://github.com/flora-pm/flora-server/issues author: Théophile Choutri