git-annex/Command/Vicfg.hs

349 lines
12 KiB
Haskell
Raw Normal View History

{- git-annex command
-
- Copyright 2012-2022 Joey Hess <id@joeyh.name>
-
- Licensed under the GNU AGPL version 3 or higher.
-}
{-# LANGUAGE RankNTypes #-}
module Command.Vicfg where
import qualified Data.Map as M
import qualified Data.Set as S
import System.Environment (getEnv)
import Data.Tuple (swap)
2012-10-03 23:13:21 +00:00
import Data.Char (isSpace)
import Data.Default
import Data.Ord
import Command
import Annex.Perms
import Types.TrustLevel
import Types.Group
import Logs.Trust
import Logs.Group
import Logs.PreferredContent
2013-10-07 21:11:13 +00:00
import Logs.Schedule
import Logs.Config
import Logs.NumCopies
2013-05-23 18:54:59 +00:00
import Types.StandardGroups
2013-10-07 21:11:13 +00:00
import Types.ScheduledActivity
import Types.NumCopies
import Remote
import Git.Types (fromConfigKey, fromConfigValue)
import qualified Utility.RawFilePath as R
cmd :: Command
cmd = command "vicfg" SectionSetup "edit configuration in git-annex branch"
paramNothing (withParams seek)
seek :: CmdParams -> CommandSeek
seek = withNothing (commandAction start)
start :: CommandStart
start = do
f <- fromRepo gitAnnexTmpCfgFile
2020-10-30 19:55:59 +00:00
let f' = fromRawFilePath f
createAnnexDirectory $ parentDir f
2012-10-03 23:13:21 +00:00
cfg <- getCfg
descs <- uuidDescriptions
2020-10-30 19:55:59 +00:00
liftIO $ writeFile f' $ genCfg cfg descs
vicfg cfg f'
stop
2012-10-03 23:13:21 +00:00
vicfg :: Cfg -> FilePath -> Annex ()
vicfg curcfg f = do
vi <- liftIO $ catchDefaultIO "vi" $ getEnv "EDITOR"
-- Allow EDITOR to be processed by the shell, so it can contain options.
2012-12-18 16:19:24 +00:00
unlessM (liftIO $ boolSystem "sh" [Param "-c", Param $ unwords [vi, shellEscape f]]) $
giveup $ vi ++ " exited nonzero; aborting"
r <- parseCfg (defCfg curcfg) <$> liftIO (readFileStrict f)
liftIO $ removeWhenExistsWith R.removeLink (toRawFilePath f)
case r of
Left s -> do
liftIO $ writeFile f s
2012-10-03 23:13:21 +00:00
vicfg curcfg f
Right newcfg -> setCfg curcfg newcfg
data Cfg = Cfg
{ cfgTrustMap :: M.Map UUID (Down TrustLevel)
, cfgGroupMap :: M.Map UUID (S.Set Group)
, cfgPreferredContentMap :: M.Map UUID PreferredContentExpression
, cfgRequiredContentMap :: M.Map UUID PreferredContentExpression
, cfgGroupPreferredContentMap :: M.Map Group PreferredContentExpression
2013-10-07 21:11:13 +00:00
, cfgScheduleMap :: M.Map UUID [ScheduledActivity]
, cfgGlobalConfigs :: M.Map ConfigKey ConfigValue
, cfgNumCopies :: Maybe NumCopies
, cfgMinCopies :: Maybe MinCopies
}
getCfg :: Annex Cfg
getCfg = Cfg
<$> (M.map Down <$> trustMapRaw) -- without local trust overrides
<*> (groupsByUUID <$> groupMap)
<*> preferredContentMapRaw
<*> requiredContentMapRaw
<*> groupPreferredContentMapRaw
2013-10-07 21:11:13 +00:00
<*> scheduleMap
<*> loadGlobalConfig
<*> getGlobalNumCopies
<*> getGlobalMinCopies
setCfg :: Cfg -> Cfg -> Annex ()
setCfg curcfg newcfg = do
let diff = diffCfg curcfg newcfg
mapM_ (uncurry trustSet) $ M.toList $ M.map (\(Down v) -> v) $ cfgTrustMap diff
mapM_ (uncurry groupSet) $ M.toList $ cfgGroupMap diff
mapM_ (uncurry preferredContentSet) $ M.toList $ cfgPreferredContentMap diff
mapM_ (uncurry requiredContentSet) $ M.toList $ cfgRequiredContentMap diff
mapM_ (uncurry groupPreferredContentSet) $ M.toList $ cfgGroupPreferredContentMap diff
mapM_ (uncurry scheduleSet) $ M.toList $ cfgScheduleMap diff
mapM_ (uncurry setGlobalConfig) $ M.toList $ cfgGlobalConfigs diff
maybe noop setGlobalNumCopies $ cfgNumCopies diff
maybe noop setGlobalMinCopies $ cfgMinCopies diff
{- Default config has all the keys from the input config, but with their
- default values. -}
defCfg :: Cfg -> Cfg
defCfg curcfg = Cfg
{ cfgTrustMap = mapdef $ cfgTrustMap curcfg
, cfgGroupMap = mapdef $ cfgGroupMap curcfg
, cfgPreferredContentMap = mapdef $ cfgPreferredContentMap curcfg
, cfgRequiredContentMap = mapdef $ cfgRequiredContentMap curcfg
, cfgGroupPreferredContentMap = mapdef $ cfgGroupPreferredContentMap curcfg
, cfgScheduleMap = mapdef $ cfgScheduleMap curcfg
, cfgGlobalConfigs = mapdef $ cfgGlobalConfigs curcfg
, cfgNumCopies = Nothing
, cfgMinCopies = Nothing
}
where
mapdef :: forall k v. Default v => M.Map k v -> M.Map k v
mapdef = M.map (const def)
diffCfg :: Cfg -> Cfg -> Cfg
diffCfg curcfg newcfg = Cfg
{ cfgTrustMap = diff cfgTrustMap
, cfgGroupMap = diff cfgGroupMap
, cfgPreferredContentMap = diff cfgPreferredContentMap
, cfgRequiredContentMap = diff cfgRequiredContentMap
, cfgGroupPreferredContentMap = diff cfgGroupPreferredContentMap
, cfgScheduleMap = diff cfgScheduleMap
, cfgGlobalConfigs = diff cfgGlobalConfigs
, cfgNumCopies = cfgNumCopies newcfg
, cfgMinCopies = cfgMinCopies newcfg
}
2012-11-12 05:05:04 +00:00
where
diff f = M.differenceWith (\x y -> if x == y then Nothing else Just x)
(f newcfg) (f curcfg)
genCfg :: Cfg -> UUIDDescMap -> String
genCfg cfg descs = unlines $ intercalate [""]
[ intro
, trust
, groups
, preferredcontent
, grouppreferredcontent
, standardgroups
, requiredcontent
, schedule
, numcopies
, globalconfigs
]
2012-11-12 05:05:04 +00:00
where
intro =
[ com "git-annex configuration"
, com ""
, com "Changes saved to this file will be recorded in the git-annex branch."
, com ""
, com "Lines in this file have the format:"
, com " setting field = value"
2012-11-12 05:05:04 +00:00
]
trust = settings cfg descs cfgTrustMap
[ com "Repository trust configuration"
2013-05-23 18:54:59 +00:00
, com "(Valid trust levels: " ++ trustlevels ++ ")"
2012-11-12 05:05:04 +00:00
]
(\(Down t, u) -> line "trust" u $ showTrustLevel t)
(\u -> lcom $ line "trust" u $ showTrustLevel def)
2013-05-23 18:54:59 +00:00
where
trustlevels = unwords $ reverse $
map showTrustLevel [minBound..maxBound]
2012-11-12 05:05:04 +00:00
groups = settings cfg descs cfgGroupMap
[ com "Repository groups"
2013-05-23 18:54:59 +00:00
, com $ "(Standard groups: " ++ grouplist ++ ")"
2012-11-12 05:05:04 +00:00
, com "(Separate group names with spaces)"
]
(\(s, u) -> line "group" u $ unwords $ map fromGroup $ S.toList s)
2012-11-12 05:05:04 +00:00
(\u -> lcom $ line "group" u "")
2013-05-23 18:54:59 +00:00
where
grouplist = unwords $ map (fromGroup . fromStandardGroup) [minBound..]
2012-11-12 05:05:04 +00:00
preferredcontent = settings cfg descs cfgPreferredContentMap
[ com "Repository preferred contents"
, com "(Set to \"standard\" to use a repository's group's preferred contents)"
]
(\(s, u) -> line "wanted" u s)
(\u -> line "wanted" u "")
requiredcontent = settings cfg descs cfgRequiredContentMap
[ com "Repository required contents" ]
(\(s, u) -> line "required" u s)
(\u -> line "required" u "")
grouppreferredcontent = settings' cfg allgroups cfgGroupPreferredContentMap
[ com "Group preferred contents"
, com "(Used by repositories with \"groupwanted\" in their preferred contents)"
2012-11-12 05:05:04 +00:00
]
(\(s, g) -> gline g s)
(\g -> gline g "")
where
gline g val = [ unwords ["groupwanted", fromGroup g, "=", val] ]
allgroups = S.unions $ stdgroups : M.elems (cfgGroupMap cfg)
stdgroups = S.fromList $ map fromStandardGroup [minBound..maxBound]
2012-11-12 05:05:04 +00:00
standardgroups =
[ com "Standard preferred contents"
, com "(Used by wanted or groupwanted expressions containing \"standard\")"
, com "(For reference only; built-in and cannot be changed!)"
]
++ map gline [minBound..maxBound]
where
gline g = com $ unwords
[ "standard"
, fromGroup (fromStandardGroup g), "=", standardPreferredContent g
]
schedule = settings cfg descs cfgScheduleMap
[ com "Scheduled activities"
2013-10-07 21:11:13 +00:00
, com "(Separate multiple activities with \"; \")"
]
(\(l, u) -> line "schedule" u $ fromScheduledActivities l)
2013-10-07 21:11:13 +00:00
(\u -> line "schedule" u "")
globalconfigs = settings' cfg S.empty cfgGlobalConfigs
[ com "Other global configuration"
]
(\(s, g) -> gline g s)
(\g -> gline g mempty)
where
gline k v = [ unwords ["config", fromConfigKey k, "=", fromConfigValue v] ]
2015-07-11 00:40:30 +00:00
line setting u val =
[ com $ "(for " ++ fromUUIDDesc (fromMaybe mempty (M.lookup u descs)) ++ ")"
2015-07-11 00:40:30 +00:00
, unwords [setting, fromUUID u, "=", val]
2012-11-12 05:05:04 +00:00
]
line' setting Nothing = com $ unwords [setting, "default", "="]
line' setting (Just val) = unwords [setting, "default", "=", val]
numcopies =
[ com "Numcopies configuration"
, line' "numcopies" (show . fromNumCopies <$> cfgNumCopies cfg)
, line' "mincopies" (show . fromMinCopies <$> cfgMinCopies cfg)
]
settings :: Ord v => Cfg -> UUIDDescMap -> (Cfg -> M.Map UUID v) -> [String] -> ((v, UUID) -> [String]) -> (UUID -> [String]) -> [String]
settings cfg descs = settings' cfg (M.keysSet descs)
settings' :: (Ord v, Ord f) => Cfg -> S.Set f -> (Cfg -> M.Map f v) -> [String] -> ((v, f) -> [String]) -> (f -> [String]) -> [String]
settings' cfg s field desc showvals showdefaults = concat
[ desc
, concatMap showvals $ sort $ map swap $ M.toList $ field cfg
, concatMap (lcom . showdefaults) missing
]
where
missing = S.toList $ s `S.difference` M.keysSet (field cfg)
lcom :: [String] -> [String]
lcom = map (\l -> if "#" `isPrefixOf` l then l else '#' : l)
{- If there's a parse error, returns a new version of the file,
- with the problem lines noted. -}
2012-10-03 23:13:21 +00:00
parseCfg :: Cfg -> String -> Either String Cfg
parseCfg defcfg = go [] defcfg . lines
2012-11-12 05:05:04 +00:00
where
go c cfg []
2013-09-25 07:09:06 +00:00
| null (mapMaybe fst c) = Right cfg
2012-11-12 05:05:04 +00:00
| otherwise = Left $ unlines $
badheader ++ concatMap showerr (reverse c)
go c cfg (l:ls) = case parse (dropWhile isSpace l) cfg of
Left msg -> go ((Just msg, l):c) cfg ls
Right cfg' -> go ((Nothing, l):c) cfg' ls
parse l cfg
| null l = Right cfg
| "#" `isPrefixOf` l = Right cfg
| null setting || null f = Left "missing field"
2015-07-11 00:40:30 +00:00
| otherwise = parsed cfg f setting val'
2012-11-12 05:05:04 +00:00
where
(setting, rest) = separate isSpace l
2015-07-11 00:40:30 +00:00
(r, val) = separate (== '=') rest
val' = trimspace val
f = reverse $ trimspace $ reverse $ trimspace r
2012-11-12 05:05:04 +00:00
trimspace = dropWhile isSpace
2015-07-11 00:40:30 +00:00
parsed cfg f setting val
| setting == "trust" = case readTrustLevel val of
Nothing -> badval "trust value" val
2012-11-12 05:05:04 +00:00
Just t ->
let m = M.insert u (Down t) (cfgTrustMap cfg)
2012-11-12 05:05:04 +00:00
in Right $ cfg { cfgTrustMap = m }
| setting == "group" =
let m = M.insert u (S.fromList $ map toGroup $ words val) (cfgGroupMap cfg)
2012-11-12 05:05:04 +00:00
in Right $ cfg { cfgGroupMap = m }
| setting == "wanted" =
2015-07-11 00:40:30 +00:00
case checkPreferredContentExpression val of
2012-11-12 05:05:04 +00:00
Just e -> Left e
Nothing ->
2015-07-11 00:40:30 +00:00
let m = M.insert u val (cfgPreferredContentMap cfg)
2012-11-12 05:05:04 +00:00
in Right $ cfg { cfgPreferredContentMap = m }
| setting == "required" =
2015-07-11 00:40:30 +00:00
case checkPreferredContentExpression val of
Just e -> Left e
Nothing ->
2015-07-11 00:40:30 +00:00
let m = M.insert u val (cfgRequiredContentMap cfg)
in Right $ cfg { cfgRequiredContentMap = m }
| setting == "groupwanted" =
2015-07-11 00:40:30 +00:00
case checkPreferredContentExpression val of
Just e -> Left e
Nothing ->
let m = M.insert (toGroup f) val (cfgGroupPreferredContentMap cfg)
in Right $ cfg { cfgGroupPreferredContentMap = m }
2015-07-11 00:40:30 +00:00
| setting == "schedule" = case parseScheduledActivities val of
Left e -> Left e
Right l ->
let m = M.insert u l (cfgScheduleMap cfg)
in Right $ cfg { cfgScheduleMap = m }
| setting == "config" =
let m = M.insert (ConfigKey (encodeBS f)) (ConfigValue (encodeBS val)) (cfgGlobalConfigs cfg)
in Right $ cfg { cfgGlobalConfigs = m }
| setting == "numcopies" = case readish val of
Nothing -> Left "parse error (expected an integer)"
Just n -> Right $ cfg { cfgNumCopies = Just (configuredNumCopies n) }
| setting == "mincopies" = case readish val of
Nothing -> Left "parse error (expected an integer)"
Just n -> Right $ cfg { cfgMinCopies = Just (configuredMinCopies n) }
2012-11-12 05:05:04 +00:00
| otherwise = badval "setting" setting
where
u = toUUID f
2012-11-12 05:05:04 +00:00
showerr (Just msg, l) = [parseerr ++ msg, l]
showerr (Nothing, l)
-- filter out the header and parse error lines
-- from any previous parse failure
| any (`isPrefixOf` l) (parseerr:badheader) = []
| otherwise = [l]
badval desc val = Left $ "unknown " ++ desc ++ " \"" ++ val ++ "\""
badheader =
[ com "** There was a problem parsing your input!"
, com "** Search for \"Parse error\" to find the bad lines."
, com "** Either fix the bad lines, or delete them (to discard your changes)."
2012-11-12 05:05:04 +00:00
]
parseerr = com "** Parse error in next line: "
2012-10-03 23:13:21 +00:00
com :: String -> String
com s = "# " ++ s