diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -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
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -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.
diff --git a/dnf-repo.cabal b/dnf-repo.cabal
--- a/dnf-repo.cabal
+++ b/dnf-repo.cabal
@@ -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)
diff --git a/src/Main.hs b/src/Main.hs
--- a/src/Main.hs
+++ b/src/Main.hs
@@ -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"]
diff --git a/src/State.hs b/src/State.hs
--- a/src/State.hs
+++ b/src/State.hs
@@ -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
diff --git a/src/Updates.hs b/src/Updates.hs
new file mode 100644
--- /dev/null
+++ b/src/Updates.hs
@@ -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"
diff --git a/src/YumRepoFile.hs b/src/YumRepoFile.hs
--- a/src/YumRepoFile.hs
+++ b/src/YumRepoFile.hs
@@ -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
