summaryrefslogtreecommitdiff
path: root/Logs
diff options
context:
space:
mode:
authorGravatar Joey Hess <joey@kitenet.net>2011-10-15 16:21:08 -0400
committerGravatar Joey Hess <joey@kitenet.net>2011-10-15 16:21:08 -0400
commit1a29b5b52eec641a5456d7c8dc24356c90107bc0 (patch)
tree0b902c278129bd085e8db986af168a4e46d3dea6 /Logs
parent279150ccd5ad937a44cbff798ab7bb118ad1dbee (diff)
reorganize log modules
no code changes
Diffstat (limited to 'Logs')
-rw-r--r--Logs/Location.hs58
-rw-r--r--Logs/Presence.hs124
-rw-r--r--Logs/Remote.hs88
-rw-r--r--Logs/Trust.hs70
-rw-r--r--Logs/UUID.hs95
-rw-r--r--Logs/UUIDBased.hs110
6 files changed, 545 insertions, 0 deletions
diff --git a/Logs/Location.hs b/Logs/Location.hs
new file mode 100644
index 000000000..4e8b2b535
--- /dev/null
+++ b/Logs/Location.hs
@@ -0,0 +1,58 @@
+{- git-annex location log
+ -
+ - git-annex keeps track of which repositories have the contents of annexed
+ - files.
+ -
+ - Repositories record their UUID and the date when they --get or --drop
+ - a value.
+ -
+ - Copyright 2010-2011 Joey Hess <joey@kitenet.net>
+ -
+ - Licensed under the GNU GPL version 3 or higher.
+ -}
+
+module Logs.Location (
+ LogStatus(..),
+ logChange,
+ readLog,
+ keyLocations,
+ loggedKeys,
+ logFile,
+ logFileKey
+) where
+
+import Common.Annex
+import qualified Git
+import qualified Annex.Branch
+import Logs.UUID
+import Logs.Presence
+
+{- Log a change in the presence of a key's value in a repository. -}
+logChange :: Git.Repo -> Key -> UUID -> LogStatus -> Annex ()
+logChange repo key u s
+ | null u = error $
+ "unknown UUID for " ++ Git.repoDescribe repo ++
+ " (have you run git annex init there?)"
+ | otherwise = addLog (logFile key) =<< logNow s u
+
+{- Returns a list of repository UUIDs that, according to the log, have
+ - the value of a key. -}
+keyLocations :: Key -> Annex [UUID]
+keyLocations = currentLog . logFile
+
+{- Finds all keys that have location log information.
+ - (There may be duplicate keys in the list.) -}
+loggedKeys :: Annex [Key]
+loggedKeys = mapMaybe (logFileKey . takeFileName) <$> Annex.Branch.files
+
+{- The filename of the log file for a given key. -}
+logFile :: Key -> String
+logFile key = hashDirLower key ++ keyFile key ++ ".log"
+
+{- Converts a log filename into a key. -}
+logFileKey :: FilePath -> Maybe Key
+logFileKey file
+ | end == ".log" = fileKey beginning
+ | otherwise = Nothing
+ where
+ (beginning, end) = splitAt (length file - 4) file
diff --git a/Logs/Presence.hs b/Logs/Presence.hs
new file mode 100644
index 000000000..7211eba03
--- /dev/null
+++ b/Logs/Presence.hs
@@ -0,0 +1,124 @@
+{- git-annex presence log
+ -
+ - This is used to store presence information in the git-annex branch in
+ - a way that can be union merged.
+ -
+ - A line of the log will look like: "date N INFO"
+ - Where N=1 when the INFO is present, and 0 otherwise.
+ -
+ - Copyright 2010-2011 Joey Hess <joey@kitenet.net>
+ -
+ - Licensed under the GNU GPL version 3 or higher.
+ -}
+
+module Logs.Presence (
+ LogStatus(..),
+ addLog,
+ readLog,
+ parseLog,
+ logNow,
+ compactLog,
+ currentLog,
+ LogLine
+) where
+
+import Data.Time.Clock.POSIX
+import Data.Time
+import System.Locale
+import qualified Data.Map as M
+
+import Common.Annex
+import qualified Annex.Branch
+
+data LogLine = LogLine {
+ date :: POSIXTime,
+ status :: LogStatus,
+ info :: String
+} deriving (Eq)
+
+data LogStatus = InfoPresent | InfoMissing | Undefined
+ deriving (Eq)
+
+instance Show LogStatus where
+ show InfoPresent = "1"
+ show InfoMissing = "0"
+ show Undefined = "undefined"
+
+instance Read LogStatus where
+ readsPrec _ "1" = [(InfoPresent, "")]
+ readsPrec _ "0" = [(InfoMissing, "")]
+ readsPrec _ _ = [(Undefined, "")]
+
+instance Show LogLine where
+ show (LogLine d s i) = unwords [show d, show s, i]
+
+instance Read LogLine where
+ -- This parser is robust in that even unparsable log lines are
+ -- read without an exception being thrown.
+ -- Such lines have a status of Undefined.
+ readsPrec _ string =
+ if length w >= 3
+ then maybe bad good pdate
+ else bad
+ where
+ w = words string
+ s = read $ w !! 1
+ i = w !! 2
+ pdate :: Maybe UTCTime
+ pdate = parseTime defaultTimeLocale "%s%Qs" $ head w
+
+ good v = ret $ LogLine (utcTimeToPOSIXSeconds v) s i
+ bad = ret $ LogLine 0 Undefined ""
+ ret v = [(v, "")]
+
+addLog :: FilePath -> LogLine -> Annex ()
+addLog file line = Annex.Branch.change file $ \s ->
+ showLog $ compactLog (line : parseLog s)
+
+{- Reads a log file.
+ - Note that the LogLines returned may be in any order. -}
+readLog :: FilePath -> Annex [LogLine]
+readLog file = parseLog <$> Annex.Branch.get file
+
+parseLog :: String -> [LogLine]
+parseLog = filter parsable . map read . lines
+ where
+ -- some lines may be unparseable, avoid them
+ parsable l = status l /= Undefined
+
+{- Generates a log file. -}
+showLog :: [LogLine] -> String
+showLog = unlines . map show
+
+{- Generates a new LogLine with the current date. -}
+logNow :: LogStatus -> String -> Annex LogLine
+logNow s i = do
+ now <- liftIO getPOSIXTime
+ return $ LogLine now s i
+
+{- Reads a log and returns only the info that is still in effect. -}
+currentLog :: FilePath -> Annex [String]
+currentLog file = map info . filterPresent <$> readLog file
+
+{- Returns the info from LogLines that are in effect. -}
+filterPresent :: [LogLine] -> [LogLine]
+filterPresent = filter (\l -> InfoPresent == status l) . compactLog
+
+{- Compacts a set of logs, returning a subset that contains the current
+ - status. -}
+compactLog :: [LogLine] -> [LogLine]
+compactLog = M.elems . foldr mapLog M.empty
+
+type LogMap = M.Map String LogLine
+
+{- Inserts a log into a map of logs, if the log has better (ie, newer)
+ - information than the other logs in the map -}
+mapLog :: LogLine -> LogMap -> LogMap
+mapLog l m =
+ if better
+ then M.insert i l m
+ else m
+ where
+ better = maybe True newer $ M.lookup i m
+ newer l' = date l' <= date l
+ i = info l
diff --git a/Logs/Remote.hs b/Logs/Remote.hs
new file mode 100644
index 000000000..47c2d7472
--- /dev/null
+++ b/Logs/Remote.hs
@@ -0,0 +1,88 @@
+{- git-annex remote log
+ -
+ - Copyright 2011 Joey Hess <joey@kitenet.net>
+ -
+ - Licensed under the GNU GPL version 3 or higher.
+ -}
+
+module Logs.Remote (
+ readRemoteLog,
+ configSet,
+ keyValToConfig,
+ configToKeyVal,
+
+ prop_idempotent_configEscape
+) where
+
+import qualified Data.Map as M
+import Data.Time.Clock.POSIX
+import Data.Char
+
+import Common.Annex
+import qualified Annex.Branch
+import Types.Remote
+import Logs.UUID
+import Logs.UUIDBased
+
+{- Filename of remote.log. -}
+remoteLog :: FilePath
+remoteLog = "remote.log"
+
+{- Adds or updates a remote's config in the log. -}
+configSet :: UUID -> RemoteConfig -> Annex ()
+configSet u c = do
+ ts <- liftIO $ getPOSIXTime
+ Annex.Branch.change remoteLog $
+ showLog showConfig . changeLog ts u c . parseLog parseConfig
+
+{- Map of remotes by uuid containing key/value config maps. -}
+readRemoteLog :: Annex (M.Map UUID RemoteConfig)
+readRemoteLog = (simpleMap . parseLog parseConfig) <$> Annex.Branch.get remoteLog
+
+parseConfig :: String -> Maybe RemoteConfig
+parseConfig = Just . keyValToConfig . words
+
+showConfig :: RemoteConfig -> String
+showConfig = unwords . configToKeyVal
+
+{- Given Strings like "key=value", generates a RemoteConfig. -}
+keyValToConfig :: [String] -> RemoteConfig
+keyValToConfig ws = M.fromList $ map (/=/) ws
+ where
+ (/=/) s = (k, v)
+ where
+ k = takeWhile (/= '=') s
+ v = configUnEscape $ drop (1 + length k) s
+
+configToKeyVal :: M.Map String String -> [String]
+configToKeyVal m = map toword $ sort $ M.toList m
+ where
+ toword (k, v) = k ++ "=" ++ configEscape v
+
+configEscape :: String -> String
+configEscape = (>>= escape)
+ where
+ escape c
+ | isSpace c || c `elem` "&" = "&" ++ show (ord c) ++ ";"
+ | otherwise = [c]
+
+configUnEscape :: String -> String
+configUnEscape = unescape
+ where
+ unescape [] = []
+ unescape (c:rest)
+ | c == '&' = entity rest
+ | otherwise = c : unescape rest
+ entity s = if ok
+ then chr (read num) : unescape rest
+ else '&' : unescape s
+ where
+ num = takeWhile isNumber s
+ r = drop (length num) s
+ rest = drop 1 r
+ ok = not (null num) &&
+ not (null r) && head r == ';'
+
+{- for quickcheck -}
+prop_idempotent_configEscape :: String -> Bool
+prop_idempotent_configEscape s = s == (configUnEscape . configEscape) s
diff --git a/Logs/Trust.hs b/Logs/Trust.hs
new file mode 100644
index 000000000..6966ffdd6
--- /dev/null
+++ b/Logs/Trust.hs
@@ -0,0 +1,70 @@
+{- git-annex trust
+ -
+ - Copyright 2010 Joey Hess <joey@kitenet.net>
+ -
+ - Licensed under the GNU GPL version 3 or higher.
+ -}
+
+module Logs.Trust (
+ TrustLevel(..),
+ trustGet,
+ trustSet,
+ trustPartition
+) where
+
+import qualified Data.Map as M
+import Data.Time.Clock.POSIX
+
+import Common.Annex
+import Types.TrustLevel
+import qualified Annex.Branch
+import qualified Annex
+
+import Logs.UUID
+import Logs.UUIDBased
+
+{- Filename of trust.log. -}
+trustLog :: FilePath
+trustLog = "trust.log"
+
+{- Returns a list of UUIDs at the specified trust level. -}
+trustGet :: TrustLevel -> Annex [UUID]
+trustGet level = M.keys . M.filter (== level) <$> trustMap
+
+{- Read the trustLog into a map, overriding with any
+ - values from forcetrust. The map is cached for speed. -}
+trustMap :: Annex TrustMap
+trustMap = do
+ cached <- Annex.getState Annex.trustmap
+ case cached of
+ Just m -> return m
+ Nothing -> do
+ overrides <- M.fromList <$> Annex.getState Annex.forcetrust
+ m <- (M.union overrides . simpleMap . parseLog parseTrust) <$>
+ Annex.Branch.get trustLog
+ Annex.changeState $ \s -> s { Annex.trustmap = Just m }
+ return m
+
+parseTrust :: String -> Maybe TrustLevel
+parseTrust s
+ | length w > 0 = readMaybe $ head w
+ -- back-compat; the trust.log used to only list trusted repos
+ | otherwise = Just Trusted
+ where
+ w = words s
+
+{- Changes the trust level for a uuid in the trustLog. -}
+trustSet :: UUID -> TrustLevel -> Annex ()
+trustSet uuid level = do
+ when (null uuid) $
+ error "unknown UUID; cannot modify trust level"
+ ts <- liftIO $ getPOSIXTime
+ Annex.Branch.change trustLog $
+ showLog show . changeLog ts uuid level . parseLog parseTrust
+ Annex.changeState $ \s -> s { Annex.trustmap = Nothing }
+
+{- Partitions a list of UUIDs to those matching a TrustLevel and not. -}
+trustPartition :: TrustLevel -> [UUID] -> Annex ([UUID], [UUID])
+trustPartition level ls = do
+ candidates <- trustGet level
+ return $ partition (`elem` candidates) ls
diff --git a/Logs/UUID.hs b/Logs/UUID.hs
new file mode 100644
index 000000000..baf665001
--- /dev/null
+++ b/Logs/UUID.hs
@@ -0,0 +1,95 @@
+{- git-annex uuids
+ -
+ - Each git repository used by git-annex has an annex.uuid setting that
+ - uniquely identifies that repository.
+ -
+ - UUIDs of remotes are cached in git config, using keys named
+ - remote.<name>.annex-uuid
+ -
+ - uuid.log stores a list of known uuids, and their descriptions.
+ -
+ - Copyright 2010-2011 Joey Hess <joey@kitenet.net>
+ -
+ - Licensed under the GNU GPL version 3 or higher.
+ -}
+
+module Logs.UUID (
+ UUID,
+ getUUID,
+ getRepoUUID,
+ getUncachedUUID,
+ prepUUID,
+ genUUID,
+ describeUUID,
+ uuidMap
+) where
+
+import qualified Data.Map as M
+import Data.Time.Clock.POSIX
+
+import Common.Annex
+import qualified Git
+import qualified Annex.Branch
+import Types.UUID
+import qualified Build.SysConfig as SysConfig
+import Config
+import Logs.UUIDBased
+
+configkey :: String
+configkey = "annex.uuid"
+
+{- Filename of uuid.log. -}
+logfile :: FilePath
+logfile = "uuid.log"
+
+{- Generates a UUID. There is a library for this, but it's not packaged,
+ - so use the command line tool. -}
+genUUID :: IO UUID
+genUUID = pOpen ReadFromPipe command params hGetLine
+ where
+ command = SysConfig.uuid
+ params = if command == "uuid"
+ -- request a random uuid be generated
+ then ["-m"]
+ -- uuidgen generates random uuid by default
+ else []
+
+getUUID :: Annex UUID
+getUUID = getRepoUUID =<< gitRepo
+
+{- Looks up a repo's UUID. May return "" if none is known. -}
+getRepoUUID :: Git.Repo -> Annex UUID
+getRepoUUID r = do
+ g <- gitRepo
+
+ let c = cached g
+ let u = getUncachedUUID r
+
+ if c /= u && u /= ""
+ then do
+ updatecache g u
+ return u
+ else return c
+ where
+ cached g = Git.configGet g cachekey ""
+ updatecache g u = when (g /= r) $ setConfig cachekey u
+ cachekey = "remote." ++ fromMaybe "" (Git.repoRemoteName r) ++ ".annex-uuid"
+
+getUncachedUUID :: Git.Repo -> UUID
+getUncachedUUID r = Git.configGet r configkey ""
+
+{- Make sure that the repo has an annex.uuid setting. -}
+prepUUID :: Annex ()
+prepUUID = whenM (null <$> getUUID) $
+ setConfig configkey =<< liftIO genUUID
+
+{- Records a description for a uuid in the log. -}
+describeUUID :: UUID -> String -> Annex ()
+describeUUID uuid desc = do
+ ts <- liftIO $ getPOSIXTime
+ Annex.Branch.change logfile $
+ showLog id . changeLog ts uuid desc . parseLog Just
+
+{- Read the uuidLog into a simple Map -}
+uuidMap :: Annex (M.Map UUID String)
+uuidMap = (simpleMap . parseLog Just) <$> Annex.Branch.get logfile
diff --git a/Logs/UUIDBased.hs b/Logs/UUIDBased.hs
new file mode 100644
index 000000000..46fa80be0
--- /dev/null
+++ b/Logs/UUIDBased.hs
@@ -0,0 +1,110 @@
+{- git-annex uuid-based logs
+ -
+ - This is used to store information about a UUID in a way that can
+ - be union merged.
+ -
+ - A line of the log will look like: "UUID[ INFO[ timestamp=foo]]"
+ - The timestamp is last for backwards compatability reasons,
+ - and may not be present on old log lines.
+ -
+ - Copyright 2011 Joey Hess <joey@kitenet.net>
+ -
+ - Licensed under the GNU GPL version 3 or higher.
+ -}
+
+module Logs.UUIDBased (
+ Log,
+ LogEntry(..),
+ parseLog,
+ showLog,
+ changeLog,
+ addLog,
+ simpleMap,
+
+ prop_TimeStamp_sane,
+ prop_addLog_sane,
+) where
+
+import qualified Data.Map as M
+import Data.Time.Clock.POSIX
+import Data.Time
+import System.Locale
+
+import Common
+import Types.UUID
+
+data TimeStamp = Unknown | Date POSIXTime
+ deriving (Eq, Ord, Show)
+
+data LogEntry a = LogEntry
+ { changed :: TimeStamp
+ , value :: a
+ } deriving (Eq, Show)
+
+type Log a = M.Map UUID (LogEntry a)
+
+tskey :: String
+tskey = "timestamp="
+
+showLog :: (a -> String) -> Log a -> String
+showLog shower = unlines . map showpair . M.toList
+ where
+ showpair (k, LogEntry (Date p) v) =
+ unwords [k, shower v, tskey ++ show p]
+ showpair (k, LogEntry Unknown v) =
+ unwords [k, shower v]
+
+parseLog :: (String -> Maybe a) -> String -> Log a
+parseLog parser = M.fromListWith best . catMaybes . map pair . lines
+ where
+ pair line
+ | null ws = Nothing
+ | otherwise = case parser $ unwords info of
+ Nothing -> Nothing
+ Just v -> Just (u, LogEntry c v)
+ where
+ ws = words line
+ u = head ws
+ end = last ws
+ c
+ | tskey `isPrefixOf` end =
+ pdate $ tail $ dropWhile (/= '=') end
+ | otherwise = Unknown
+ info
+ | c == Unknown = drop 1 ws
+ | otherwise = drop 1 $ init ws
+ pdate s = case parseTime defaultTimeLocale "%s%Qs" s of
+ Nothing -> Unknown
+ Just d -> Date $ utcTimeToPOSIXSeconds d
+
+changeLog :: POSIXTime -> UUID -> a -> Log a -> Log a
+changeLog t u v = M.insert u $ LogEntry (Date t) v
+
+{- Only add an LogEntry if it's newer (or at least as new as) than any
+ - existing LogEntry for a UUID. -}
+addLog :: UUID -> LogEntry a -> Log a -> Log a
+addLog = M.insertWith best
+
+{- Converts a Log into a simple Map without the timestamp information.
+ - This is a one-way trip, but useful for code that never needs to change
+ - the log. -}
+simpleMap :: Log a -> M.Map UUID a
+simpleMap = M.map value
+
+best :: LogEntry a -> LogEntry a -> LogEntry a
+best new old
+ | changed old > changed new = old
+ | otherwise = new
+
+-- Unknown is oldest.
+prop_TimeStamp_sane :: Bool
+prop_TimeStamp_sane = Unknown < Date 1
+
+prop_addLog_sane :: Bool
+prop_addLog_sane = newWins && newestWins
+ where
+ newWins = addLog "foo" (LogEntry (Date 1) "new") l == l2
+ newestWins = addLog "foo" (LogEntry (Date 1) "newest") l2 /= l2
+
+ l = M.fromList [("foo", LogEntry (Date 0) "old")]
+ l2 = M.fromList [("foo", LogEntry (Date 1) "new")]