packages feed

dnf-repo 0.6.4 → 0.7

raw patch · 7 files changed

+460/−120 lines, 7 filesdep +aesondep +edit-distancedep +time

Dependencies added: aeson, edit-distance, time, typed-process

Files

ChangeLog.md view
@@ -1,5 +1,12 @@ # dnf-repo releases +## 0.7 (2026-09-28)+- new "updates" custom command provides nicer "advisory info" output+- allow but warn about non-/usr/bin/dnf*+- support `/usr/share/dnf5/repos.d` (and `/etc/distro.repos.d`)+- support `/etc/dnf/repos.override.d` (and `/usr/share/dnf5/repos.override.d`)+- suggest misspelled command or offer to install if dnf5 command missing+ ## 0.6.4 (2026-06-22) - fix handling of --only for enabled repos - don't run dnf from /etc/yum.repos.d/ to unbreak relative rpm paths
README.md view
@@ -15,37 +15,37 @@  There are also smart options to enable/disable testing repos (and even source/debuginfo repos),-and also to add Copr repo or Koji repo file.+and also to add Copr repo or Koji repo files.  ## Help  `$ dnf-repo --version`  ```-0.6.4+0.7 ``` `$ dnf-repo --help`  ``` DNF wrapper repo tool -Usage: dnf-repo [--version] [-n|--dryrun] [-q|--quiet] [-D|--debug] [-l|--list] +Usage: dnf-repo [--version] [-n|--dryrun] [-q|--quiet] [-D|--debug] [-l|--list]                 [-s|--save] [-4|--dnf4] [(-w|--weak-deps) | (-W|--no-weak-deps)]-                [--exact] -                [(-d|--disable REPOPAT) | (-e|--enable REPOPAT) | -                  (-o|--only REPOPAT) | --base-mirror URL | -                  (-x|--expire REPOPAT) | (-X|--clear-expires) | -                  (-E|--delete-repofile REPOPAT) | (-z|--timestamp REPOPAT) | -                  (-t|--enable-testing) | (-T|--disable-testing) | -                  (-m|--enable-modular) | (-M|--disable-modular) | -                  --enable-debuginfo | --disable-debuginfo | --enable-source | -                  --disable-source | (-c|--add-copr [SERVER/]COPR/PROJECT|URL) -                  [--osname OSNAME] [--copr-releasever RELEASEVER] |-                  (-k|--add-koji REPO) | (-r|--add-repofile REPOFILEURL) +                [--exact]+                [(-d|--disable REPOPAT) | (-e|--enable REPOPAT) |+                  (-o|--only REPOPAT) | (-x|--expire REPOPAT) |+                  (-X|--clear-expires) | (-E|--delete-repofile REPOPAT) |+                  (-z|--timestamp REPOPAT) | (-t|--enable-testing) |+                  (-T|--disable-testing) | (-m|--enable-modular) |+                  (-M|--disable-modular) | --enable-debuginfo |+                  --disable-debuginfo | --enable-source | --disable-source |+                  (-c|--add-copr [SERVER/]COPR/PROJECT|URL) [--osname OSNAME]+                  [--copr-releasever RELEASEVER] |+                  (-k|--add-koji REPO) | (-r|--add-repofile REPOFILEURL)                   [--repo-releasever RELEASEVER] |                   (-u|--repourl URL)] [DNFARGS] -  see https://github.com/juhp/dnf-repo#readme+  see https://github.com/juhp/dnf-repo#readme ; 'updates' to show updates info  Available options:   -h,--help                Show this help text@@ -62,8 +62,6 @@   -d,--disable REPOPAT     Disable repos   -e,--enable REPOPAT      Enable repos   -o,--only REPOPAT        Only use matching repos-  --base-mirror URL        Override fedora metalink with a mirror baseurl (eg-                           'cloudfront', or reset with 'metalink')   -x,--expire REPOPAT      Expire repo cache (dnf4)   -X,--clear-expires       Undo cache expirations (dnf4)   -E,--delete-repofile REPOPAT@@ -122,13 +120,15 @@ ``` (note the copr repo is not permanently enabled). -Also you can use the equivalent abbreviation: `dnf-repo -c varlad/helix install` if the package and repo have the same name.+If the package and repo have the same name, you can use also the equivalent abbreviation: `dnf-repo -c varlad/helix install`.  Later update with the copr: ```shellsession $ dnf-repo -e helix update ``` +See more below about repo name matching.+ ### Changing system repo config Disable fedora updates-testing and cisco openh264 repos permanently: ```shellsession@@ -145,9 +145,9 @@ ```  ### Switch system from rawhide-Switch a system from Rawhide to F40:+Switch a system from Rawhide to F45: ```shellsession-$ dnf-repo -d rawhide -e fedora distrosync --releasever 40 fedora-\*+$ dnf-repo -d rawhide -e fedora distrosync --releasever 45 fedora-\* with disabled 'rawhide' with enabled 'fedora' @@ -174,20 +174,36 @@ `--repo=`, etc, so as usual later settings will overrule conflicting earlier settings. +## dnf repo directories+dnf5 repository definitions are read from `/etc/yum.repos.d`,+`/etc/distro.repos.d`, and `/usr/share/dnf5/repos.d`+(the first definition of a repo id wins).++### Repo overrides+dnf5 `repos.override.d` drop-ins are applied for `enabled` and `baseurl`+(`/etc/dnf/repos.override.d/` masks the same filename under+`/usr/share/dnf5/repos.override.d/`).+Other override keys (for example `skip_if_unavailable`) are not tracked.+`--save` on dnf5 writes `/etc/dnf/repos.override.d/99-config_manager.repo`+via `config-manager setopt`.++## Other commands+- "distrosync" is mapped to "distro-sync".++- "updates" command++This makes dnf5's "advisory info" (updateinfo) output concise and readable.+It parse `--json` output and prettyprints without any long list of rpms.+ ## Installation `dnf-repo` is packaged in Fedora and EPEL  ## Building Use {cabal,stack,cabal-rpm} install. -## Known issues-Currently dnf5's `/etc/dnf/repos.override.d/` (set by config-manager) is-ignored, and as of 0.6.3 `dnf-repo --save` will edit .repo files directly-as dnf4 does.- ## Contributing dnf-repo is distributed under the GPL license version 3 or later.  The source repository is https://github.com/juhp/dnf-repo/ -Contributions including reports and suggestions for improvement are welcome.+Contributions, reports and suggestions for improvement are welcome.
dnf-repo.cabal view
@@ -1,5 +1,5 @@ name:                dnf-repo-version:             0.6.4+version:             0.7 synopsis:            A dnf wrapper with fine control of enabled repos description:         A command-line wrapper of the dnf package manager to@@ -41,12 +41,15 @@                        State                        Sudo                        TimeStamp+                       Updates                        YumRepoFile   autogen-modules:     Paths_dnf_repo   hs-source-dirs:      src   build-depends:       base < 5,+                       aeson,                        curl,                        directory,+                       edit-distance,                        extra,                        filepath,                        Glob,@@ -54,7 +57,9 @@                        safe,                        simple-cmd >= 0.2.2,                        simple-cmd-args >= 0.1.8,-                       simple-prompt >= 0.2+                       simple-prompt >= 0.2,+                       time,+                       typed-process   default-language:    Haskell2010   ghc-options:         -Wall   if impl(ghc >= 8.0)
src/Main.hs view
@@ -12,14 +12,14 @@ import Data.Tuple.Extra (fst3) import Network.Curl (curlGetString, CurlCode(CurlOK)) import Safe (lastMay)-import SimpleCmd (cmdMaybe, error', warning, (+-+),+import SimpleCmd (cmdFull, cmdMaybe, error', warning, (+-+), #if MIN_VERSION_simple_cmd(0,2,4)                   filesWithExtension #endif                  ) import SimpleCmdArgs import SimplePrompt (yesNo)-import System.Directory (doesDirectoryExist, doesFileExist, findFile,+import System.Directory (doesDirectoryExist, doesFileExist, findExecutable,                          withCurrentDirectory, #if !MIN_VERSION_simple_cmd(0,2,4)                          listDirectory@@ -29,14 +29,16 @@ import System.FilePath import System.IO (hSetBuffering, stdout, BufferMode(NoBuffering)) import System.IO.Extra (withTempDir)+import Text.EditDistance -import Paths_dnf_repo (version) import CoprRepo import ExpireRepos import KojiRepo+import Paths_dnf_repo (version) import State import Sudo import TimeStamp+import Updates import YumRepoFile  main :: IO ()@@ -44,7 +46,7 @@   checkSudo   simpleCmdArgs' (Just version)     "DNF wrapper repo tool"-    "see https://github.com/juhp/dnf-repo#readme" $+    "see https://github.com/juhp/dnf-repo#readme ; 'updates' to show updates info" $     runMain     <$> switchWith 'n' "dryrun" "Dry run"     <*> switchWith 'q' "quiet" "Suppress necessary output"@@ -105,20 +107,21 @@ yumReposD :: String yumReposD = "/etc/yum.repos.d" -checkSystemPathFile :: String -> IO (Maybe String)-checkSystemPathFile prog = do-  let path = splitOn ":" "/usr/sbin:/usr/bin"-  fmap takeFileName <$> findFile path prog+-- dnf5 repos.d order: the first definition of a repo id wins+dnf5RepoDirs :: [FilePath]+dnf5RepoDirs =+  [yumReposD, "/etc/distro.repos.d", "/usr/share/dnf5/repos.d"]  -- FIXME both enabling and disabled at the same time -- FIXME confirm repos if many -- FIXME --disable-non-cores (modular,testing,cisco, etc)--- FIXME support dnf5 /etc/dnf/repos.override.d/99-config_manager.repo runMain :: Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Maybe Bool -> Bool         -> [Mode] -> [String] -> IO () runMain dryrun quiet debug listrepos save dnf4 mweakdeps exact modes args = do   hSetBuffering stdout NoBuffering-  unlessM (doesDirectoryExist yumReposD) $+  mpkgmgr <- detectPkgMgr quiet dnf4+  let repoDirs = if mpkgmgr == Just Dnf5 then dnf5RepoDirs else [yumReposD]+  unlessM (anyM doesDirectoryExist repoDirs) $     error' $ yumReposD +-+ "not found!"   (nameStates,actions) <-     withCurrentDirectory yumReposD $ do@@ -131,10 +134,16 @@         AddRepo repo mrelease ->           addRepoFile dryrun debug mrelease repo         _ -> return ()-    repofiles <- filesWithExtension "./" "repo"+    existing <- filterM doesDirectoryExist repoDirs+    loaded <- concatMapM readRepoDir existing     when debug $ print modes-    nameStates <- sort <$> concatMapM readRepos repofiles-    when debug $ mapM_ print nameStates+    unique <- dropDuplicateRepos loaded+    overridden <-+      if mpkgmgr == Just Dnf5+      then applyOverrideFiles unique+      else return unique+    let nameStates = sort overridden+    when debug $ mapM_ printRepo nameStates     let actions = selectRepo exact nameStates modes         moreoutput = not (null args) || null actions || listrepos     when (debug && not (null modes)) $@@ -164,61 +173,85 @@     when (or outputs && (save || moreoutput) && not quiet) $       warning ""     return (nameStates,actions)-  mpkgmgr <--    if dnf4-    then do-      mdnf3 <- checkSystemPathFile "dnf-3"-      case mdnf3 of-        Just _ -> return $ Just Dnf4-        Nothing -> error' "dnf-3 not found"-    else do-      mdnf5 <- checkSystemPathFile "dnf5"-      case mdnf5 of-        Just _ -> return $ Just Dnf5-        Nothing -> do-          mdnf3 <- checkSystemPathFile "dnf-3"-          case mdnf3 of-            Just _ -> return $ Just Dnf4-            Nothing -> return Nothing   when save $     if null actions       then putStrLn "no changes to save"       else do-      let changes = concatMap (saveRepo (mpkgmgr == Just Dnf4)) actions-      unless (null changes) $ do-        ok <- yesNo $ "Save changed repo" +-+ "enabled state" ++ ['s' | length changes > 1]-        when ok $-          if mpkgmgr == Just Dnf4-          then doSudo dryrun debug (pkgMgrCmd Dnf4) $ "config-manager" : changes-          else doSudo dryrun debug "sh" ["-c", unwords $ "sed" : "-i" : map show changes ++ ["/etc/yum.repos.d/*.repo"]]-  if null args-    then-    when (null actions || listrepos) $ do-    when save $ putStrLn ""-    listRepos $ map (updateState actions) nameStates-    else do-    when save $ putStrLn ""-    case mpkgmgr of-      Just dnf ->-        let repoargs = mapMaybe changeRepo actions-            weakdeps = maybe [] (\w -> ["--setopt=install_weak_deps=" ++ show w]) mweakdeps-            quietopt = if quiet then ("-q" :) else id-            cachedir = ["--setopt=cachedir=/var/cache/dnf" </> relver | relver <- maybeToList (maybeReleaseVer args)]-            extraargs =-              -- special case for "dnf-repo [-c owner/project|-e repo] install"-              case actions of-                [action] ->-                  case action of-                    Enable repo True | args == ["install"] ->-                                         [takeWhileEnd (/= ':') repo]-                    _ -> []-                _ -> []-            -- FIXME default to "install" of no command?-        in doSudo dryrun debug (pkgMgrCmd dnf) $-           quietopt repoargs ++ cachedir ++ weakdeps ++ map mungeArg args ++-           extraargs-      -- FIXME rpm-ostree install supports --enablerepo-      Nothing -> error' "missing dnf (rpm-ostree is not supported)"+      let named = mapMaybe saveAction actions+          confirmSave n =+            yesNo $ "Save changed repo" +-+ "enabled state" ++ ['s' | length n > 1]+      case mpkgmgr of+        Just Dnf5 ->+          let optvals = mapMaybe setoptVal named in+            unless (null optvals) $+            whenM (confirmSave optvals) $+            doSudo dryrun debug (pkgMgrCmd Dnf5) $+            ["config-manager", "setopt", "--create-missing-dir"] ++ optvals+        _ -> do+          let (etcrepos, outside) =+                partition (repoInYumReposD nameStates . fst) named+          unless (null outside) $+            warning $ "not saving" +-+ unwords (map fst outside) +++              ": defined outside" +-+ yumReposD+          let changes = concatMap (saveRepo (mpkgmgr == Just Dnf4) . snd) etcrepos+          unless (null changes) $+            whenM (confirmSave changes) $+            if mpkgmgr == Just Dnf4+            then doSudo dryrun debug (pkgMgrCmd Dnf4) $ "config-manager" : changes+            else doSudo dryrun debug "sh" ["-c", unwords $ "sed" : "-i" : map show changes ++ ["/etc/yum.repos.d/*.repo"]]+  case args of+    [] ->+      when (null actions || listrepos) $ do+      when save $ putStrLn ""+      listRepos $ map (updateState actions) nameStates+    (c:as) -> do+      when save $ putStrLn ""+      case mpkgmgr of+        -- FIXME rpm-ostree install supports --enablerepo+        Nothing -> error' "missing dnf (rpm-ostree is not supported)"+        Just dnf ->+          let repoargs = mapMaybe changeRepo actions+              weakdeps = maybe [] (\w -> ["--setopt=install_weak_deps=" ++ show w]) mweakdeps+              quietopt = if quiet then ("-q" :) else id+              cachedir = ["--setopt=cachedir=/var/cache/dnf" </> relver | relver <- maybeToList (maybeReleaseVer args)]+              extraargs =+                -- special case for "dnf-repo [-c owner/project|-e repo] install"+                case actions of+                  [action] ->+                    case action of+                      Enable repo True | args == ["install"] ->+                                           [takeWhileEnd (/= ':') repo]+                      _ -> []+                  _ -> []+          in+            if c `elem` ["updates","advisories","updateinfos"] && dnf == Dnf5+            then+              printUpdates as+            else do+              args' <-+                if null as && dnf == Dnf5+                then do+                  if c `elem` dnfCommands+                    then return args+                    else+                    let close = filter (\c' -> levenshteinDistance defaultEditCosts c c' < 3) dnfCommands in+                      if null close+                      then do+                        installed <- isInstalled c+                        if installed+                          then error' $ c +-+ "is already installed: missing command"+                          else do+                          ok <- yesNo $ "Do you want to install" +-+ c+                          if ok+                            then return $ "install" : args+                            else return args+                      else do+                        warning $ "Did you mean:" +-+ unwords close +-+ "?"+                        return args+                else return args+              doSudo dryrun debug (pkgMgrCmd dnf) $+                quietopt repoargs ++ cachedir ++ weakdeps ++ map mungeArg args'+                ++ extraargs   where     mungeArg :: String -> String     mungeArg "distrosync" = "distro-sync"@@ -255,7 +288,7 @@ listRepos repoStates = do   let (on,off) =         -- can't this be simplified?-        bimap (map fst) (map fst) $ partition (fst3 . snd) repoStates+        bimap (map repoName) (map repoName) $ partition (fst3 . repoState) repoStates   putStrLn "Enabled:"   mapM_ putStrLn on   putStrLn ""@@ -264,7 +297,7 @@  deleteRepo :: Bool -> Bool -> FilePath -> IO () deleteRepo dryrun debug repofile = do-  mowned <- cmdMaybe "rpm" ["-qf", yumReposD </> repofile]+  mowned <- cmdMaybe "rpm" ["-qf", repofile]   case mowned of     Just owner -> warning $ repofile +-+ "owned by" +-+ owner     Nothing -> do@@ -272,6 +305,100 @@       when ok $ do         doSudo dryrun debug "rm" [repofile] +-- FIXME should default to system default+detectPkgMgr :: Bool -> Bool -> IO (Maybe PkgMgr)+detectPkgMgr quiet dnf4 =+  if dnf4+  then do+    mdnf3 <- checkSystemPathFile "dnf-3"+    case mdnf3 of+      Just _ -> return $ Just Dnf4+      Nothing -> error' "dnf-3 not found"+  else do+    mdnf5 <- checkSystemPathFile "dnf5"+    case mdnf5 of+      Just _ -> return $ Just Dnf5+      Nothing -> do+        mdnf3 <- checkSystemPathFile "dnf-3"+        case mdnf3 of+          Just _ -> return $ Just Dnf4+          Nothing -> return Nothing+  where+    checkSystemPathFile :: String -> IO (Maybe String)+    checkSystemPathFile prog = do+      mpath <- findExecutable prog+      case mpath of+        Nothing -> return Nothing+        Just path -> do+          let syspath = "/usr/bin" </> prog+          unless (path == syspath) $ do+            exists <- doesFileExist syspath+            when exists $+              unless quiet $+              warning $ path +-+ "overrides" +-+ syspath+          return $ Just path++readRepoDir :: FilePath -> IO [RepoState]+readRepoDir dir = do+  files <- filesWithExtension dir "repo"+  concatMapM (readRepos . (dir </>)) files++-- /etc file masks the same filename under /usr; then alphabetical order.+overrideDirs :: [FilePath]+overrideDirs =+  ["/usr/share/dnf5/repos.override.d", "/etc/dnf/repos.override.d"]++applyOverrideFiles :: [RepoState] -> IO [RepoState]+applyOverrideFiles states = do+  files <- overrideFiles+  ovs <- concatMapM readOverrides files+  return $ applyOverrides ovs states++overrideFiles :: IO [FilePath]+overrideFiles = do+  named <- concatMapM listRepoFiles overrideDirs+  return $ map snd $ sortOn fst $ foldl' mask [] named+  where+    -- later dir (/etc) replaces an earlier file of the same name+    mask acc (name, path) =+      (name, path) : filter ((/= name) . fst) acc++    listRepoFiles :: FilePath -> IO [(FilePath, FilePath)]+    listRepoFiles dir = do+      exists <- doesDirectoryExist dir+      if exists+        then map (\f -> (f, dir </> f)) <$> filesWithExtension dir "repo"+        else return []++-- First definition of a repo id wins, matching dnf5 reposdir order.+dropDuplicateRepos :: [RepoState] -> IO [RepoState]+dropDuplicateRepos = go []+  where+    go _ [] = return []+    go seen (rs@(RepoState name (_,file,_)):rest) =+      case lookup name seen of+        Just kept -> do+          warning $ name +-+ "already defined in" +-+ kept +++            ", ignoring" +-+ file+          go seen rest+        Nothing -> (rs :) <$> go ((name,file):seen) rest++saveAction :: ChangeEnable -> Maybe (String, ChangeEnable)+saveAction a@(Enable r True) = Just (r, a)+saveAction a@(Disable r True) = Just (r, a)+saveAction _ = Nothing++setoptVal :: (String, ChangeEnable) -> Maybe String+setoptVal (r, Disable _ _) = Just $ r ++ ".enabled=0"+setoptVal (r, Enable _ _) = Just $ r ++ ".enabled=1"+setoptVal (_, _) = Nothing++repoInYumReposD :: [RepoState] -> String -> Bool+repoInYumReposD states name =+  case lookupRepo name states of+    Just (_, file, _) -> takeDirectory file == yumReposD+    Nothing -> False+ #if !MIN_VERSION_simple_cmd(0,2,4) filesWithExtension :: FilePath -> String -> IO [FilePath] filesWithExtension dir ext =@@ -311,3 +438,15 @@ isExtensionOf ext@('.':_) = isSuffixOf ext . takeExtensions isExtensionOf ext         = isSuffixOf ('.':ext) . takeExtensions #endif++isInstalled :: String -> IO Bool+isInstalled pkg = do+  (ok,_,_) <- cmdFull "rpm" ["-q", pkg] ""+  return ok++-- generated by dnf5-commands.awk+-- FIXME could reverse map "onetwo" to "one-two" - would also allow dropping the hard coded "distrosync" hack+dnfCommands :: [String]+dnfCommands =+  -- dnf5-5.4.5.0+  ["do","install","upgrade","remove","distro-sync","downgrade","reinstall","debuginfo-install","swap","mark","autoremove","provides","replay","check-upgrade","check","leaves","repoquery","search","list","info","status","group","environment","module","history","repo","advisory","versionlock","system-upgrade","offline-distrosync","offline-upgrade","offline","config-manager","check-update","dg","dsync","grp","if","in","ls","mc","rei","repoinfo","repolist","rm","rq","se","up","update","updateinfo","upgrade-minimal","clean","download","makecache","builddep","changelog","copr","needs-restarting","repoclosure","repomanage","reposync","build-dep"]
src/State.hs view
@@ -3,12 +3,13 @@ module State (   Mode(..),   SpecificChange(..),-  ChangeEnable(Enable,Expire,UnExpire,Delete,TimeStamp),+  ChangeEnable(Disable,Enable,Expire,UnExpire,Delete,TimeStamp),   printAction,   reduceOutput,   selectRepo,   changeRepo,   saveRepo,+  lookupRepo,   updateState,   expiring,   deleting,@@ -31,7 +32,7 @@ import System.FilePath (takeBaseName) import System.FilePath.Glob (compile, match) -import YumRepoFile (RepoState)+import YumRepoFile (RepoState(..))  data Mode = AddCopr String (Maybe String) (Maybe String) | AddKoji String           | AddRepo String (Maybe String) | RepoURL String@@ -177,12 +178,12 @@  updateState :: [ChangeEnable] -> RepoState -> RepoState updateState [] rs = rs-updateState (ce:ces) re@(repo,(enabled,file,url)) =+updateState (ce:ces) re@(RepoState repo (enabled,file,url)) =   case ce of-    Disable r True | r == repo && enabled -> (repo,(False,file,url))-    Enable r True | r == repo && not enabled -> (repo,(True,file,url))-    Only r True | r == repo && not enabled -> (repo,(True,file,url))-    Only r True | r /= repo && enabled -> (repo,(False,file,url))+    Disable r True | r == repo && enabled -> RepoState repo (False,file,url)+    Enable r True | r == repo && not enabled -> RepoState repo (True,file,url)+    Only r True | r == repo && not enabled -> RepoState repo (True,file,url)+    Only r True | r /= repo && enabled -> RepoState repo (False,file,url)     _ -> updateState ces re  selectRepo :: Bool -> [RepoState] -> [Mode] -> [ChangeEnable]@@ -215,7 +216,7 @@      selectRepoMode :: Mode -> [ChangeEnable] -> RepoState                    -> Maybe ChangeEnable-    selectRepoMode mode acc (name,(enabled,file,murl)) =+    selectRepoMode mode acc (RepoState name (enabled,file,murl)) =       case mode of         AddCopr repo _ _ ->           maybeChange repo isSuffixOf (not enabled) (Enable name)@@ -266,7 +267,7 @@      repoStatus :: [ChangeEnable] -> String -> Bool     repoStatus acc repo =-      case lookup repo repostates of+      case lookupRepo repo repostates of         Just (enabled,_,_) ->           enabled || Enable repo True `elem` acc         Nothing -> False@@ -302,3 +303,10 @@ removeInfix :: String -> String-> String removeInfix inf orig =   maybe orig (uncurry (++)) $ stripInfix inf orig++lookupRepo :: String -> [RepoState] -> Maybe (Bool, FilePath, Maybe String)+lookupRepo _ [] = Nothing+lookupRepo name (RepoState n r : rs) =+  if name == n+  then Just r+  else lookupRepo name rs
+ src/Updates.hs view
@@ -0,0 +1,86 @@+{-# LANGUAGE OverloadedStrings #-}++module Updates (printUpdates)+where++import Data.Aeson (FromJSON, eitherDecode', parseJSON, withObject, (.:))+import Data.List.Extra (intercalate, sortOn, trim)+import Data.Maybe (maybeToList)+import Data.String (fromString)+import Data.Time (UTCTime)+import Data.Time.Clock.POSIX (posixSecondsToUTCTime)+import SimpleCmd (error', (+-+))+import System.Process.Typed (proc, readProcessStdout_)++printUpdates :: [String] -> IO ()+printUpdates args = do+  json <- readProcessStdout_ $ proc "sudo" $ ["dnf5", "-q", "advisory", "info", "--json"] ++ args+  case renderUpdates json of+    Left err -> error' $ "failed to parse advisory json:" +-+ err+    Right out -> putStr out+  where+    -- Compact "dnf5 advisory info --json"+    renderUpdates json =+      case eitherDecode' json of+        Right arr -> Right $ unlines $ map formatUpdate $ sortOn advName arr+        Left err -> Left err++-- FIXME use local time+formatUpdate :: Advisory -> String+formatUpdate adv =+  let name = advName adv+      title = "#" +-+ trim (advTitle adv)+      typ = advType adv+      sev = advSeverity adv+      date = show (advIssued adv)+      desc = trim $ advDescription adv+      refs = map formatRef (advReferences adv)+      prose = lines desc ++ refs+      body = map ("  " ++) prose+  in unlines $+     title :+     intercalate "  " ([' ' : name, typ] ++ maybeToList sev ++ [date]) : body+  where+    formatRef ref =+      refUrl ref +-+ ":" +-+ refTitle ref++data Advisory = Advisory {+  advName :: String,+  advTitle :: String,+  advSeverity :: Maybe String,+  advType :: String,+  advIssued :: UTCTime,+  advDescription :: String,+  advReferences :: [Reference]+  }++data Reference = Reference {+  refUrl :: String,+  refTitle :: String+  }++instance FromJSON Advisory where+  parseJSON =+    withObject "Advisory" $ \obj ->+      Advisory+      <$> obj .: "Name"+      <*> obj .: "Title"+      <*> (maybeSeverity <$> obj .: "Severity")+      <*> obj .: "Type"+      <*> (secondsToUTC <$> obj .: "Issued")+      <*> obj .: "Description"+      <*> obj .: "references"+    where+      maybeSeverity s =+        if s `elem` ["","None"]+        then Nothing+        else Just s++      secondsToUTC = posixSecondsToUTCTime . fromInteger++instance FromJSON Reference where+  parseJSON =+    withObject "Reference" $ \obj ->+      Reference+      <$> obj .: fromString "Url"+      <*> obj .: fromString "Title"
src/YumRepoFile.hs view
@@ -1,16 +1,34 @@+{-# LANGUAGE CPP #-}+ module YumRepoFile (   readRepos,-  RepoState+  RepoState(..),+  printRepo,+  applyOverrides,+  readOverrides   ) where -import Data.List.Extra (breakOn, isInfixOf, isPrefixOf, splitOn, trim, trimEnd)-import SimpleCmd (error', (+-+))+import Control.Applicative ((<|>))+import Control.Monad (foldM)+import Data.List.Extra (breakOn, isInfixOf, isPrefixOf, splitOn, trim, trimEnd,+#if !MIN_VERSION_base(4,20,0)+                        foldl',+#endif+                        unsnoc)+import Data.Maybe (fromMaybe)+import SimpleCmd (error', warning, (+-+))+import System.FilePath.Glob (compile, match) -type RepoState = (String, -- reponame-                  (Bool, -- enabled-                   FilePath, -- repofile-                   Maybe String)) -- baseurl+data RepoState =+  RepoState+  { repoName :: String+  , repoState :: (Bool, -- enabled+                  FilePath, -- repofile+                  Maybe String -- baseurl+                 )+  }+  deriving (Eq, Ord)  readRepos :: FilePath -> IO [RepoState] readRepos file =@@ -19,7 +37,7 @@ -- parse ini parseRepos :: FilePath -> [String] -> [RepoState] parseRepos file ls =-  case nextSection ls of+  case nextSection file ls of     Nothing -> []     Just (section,rest) ->       let (mbaseurl,rest1) =@@ -40,14 +58,75 @@                       "0" -> (False,more')                       _ -> error' $ "strange enabled state" +-+ e +-+ "for" +-+ section                   _ -> error' $ "unknown enabled state" +-+ e +-+ "for" +-+ section-      in (section,(enabled,file,mbaseurl)) : parseRepos file more+      in RepoState section (enabled,file,mbaseurl) : parseRepos file more++nextSection :: FilePath -> [String] -> Maybe (String,[String])+nextSection _ [] = Nothing+nextSection file (l:ls') =+  case trimEnd l of+    ('[' : rest) ->+      case unsnoc rest of+        Just (rest',']') -> Just (rest', ls')+        _ ->  error' $ "bad section" +-+ l +-+ "in" +-+ file+    _ -> nextSection file ls'++printRepo :: RepoState -> IO ()+printRepo (RepoState n s) =+  print (n,s)++-- An override section may set only enabled and/or baseurl (or neither).+data RepoOverride = RepoOverride String (Maybe Bool) (Maybe String)++readOverrides :: FilePath -> IO [RepoOverride]+readOverrides file = do+  ls <- lines <$> readFile file+  parseOverrides file ls++parseOverrides :: FilePath -> [String] -> IO [RepoOverride]+parseOverrides file ls =+  case nextSection file ls of+    Nothing -> return []+    Just (section, rest) -> do+      let (body, more) = break isHeader rest+      ov <- sectionOverride file (trim section) body+      (ov :) <$> parseOverrides file more   where-    nextSection :: [String] -> Maybe (String,[String])-    nextSection [] = Nothing-    nextSection (l:ls') =+    isHeader l =       case trimEnd l of-        ('[' : rest) ->-          if last rest == ']'-          then Just (init rest, ls')-          else error' $ "bad section" +-+ l +-+ "in" +-+ file-        _ -> nextSection ls'+        '[':_ -> True+        _ -> False++sectionOverride :: FilePath -> String -> [String] -> IO RepoOverride+sectionOverride file section body = do+  (menable, murl) <- foldM add (Nothing, Nothing) body+  return $ RepoOverride section menable murl+  where+    add (menable, murl) line =+      case splitKv line of+        Just ("enabled", v) ->+          case v of+            "1" -> return (Just True, murl)+            "0" -> return (Just False, murl)+            _ -> do+              warning $ "strange enabled state" +-+ v +-+ "for" +-+ section +-++                "in" +-+ file+              return (menable, murl)+        Just ("baseurl", v) -> return (menable, Just v)+        _ -> return (menable, murl)++    splitKv line =+      case breakOn "=" line of+        (k, '=':v) -> Just (trim k, trim v)+        _ -> Nothing++applyOverrides :: [RepoOverride] -> [RepoState] -> [RepoState]+applyOverrides ovs states = foldl' applyOne states ovs+  where+    applyOne ss (RepoOverride pat men murl) =+      let compiled = compile pat+      in map (apply compiled men murl) ss++    apply compiled men murl rs@(RepoState name (en, file, url))+      | match compiled name =+          RepoState name (fromMaybe en men, file, murl <|> url)+      | otherwise = rs