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 +7/−0
- README.md +42/−26
- dnf-repo.cabal +7/−2
- src/Main.hs +205/−66
- src/State.hs +17/−9
- src/Updates.hs +86/−0
- src/YumRepoFile.hs +96/−17
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