use a state monad

enormous reworking
This commit is contained in:
Joey Hess 2010-10-13 21:28:47 -04:00
parent e5c1db355f
commit b160748516
11 changed files with 251 additions and 157 deletions

138
Annex.hs
View file

@ -12,6 +12,7 @@ module Annex (
annexPullRepo
) where
import Control.Monad.State (liftIO)
import System.Posix.Files
import System.Directory
import Data.String.Utils
@ -25,22 +26,27 @@ import UUID
import LocationLog
import Types
{- On startup, examine the git repo, prepare it, and record state for
- later. -}
startAnnex :: IO State
{- Create and returns an Annex state object.
- Examines and prepares the git repo.
-}
startAnnex :: IO AnnexState
startAnnex = do
r <- gitRepoFromCwd
r' <- gitConfigRead r
r'' <- prepUUID r'
gitSetup r''
return State {
repo = r',
backends = parseBackendList $ gitConfig r' "annex.backends" ""
}
g <- gitRepoFromCwd
let s = makeAnnexState g
(_,s') <- runAnnexState s (prep g)
return s'
where
prep g = do
-- setup git and read its config; update state
liftIO $ gitSetup g
g' <- liftIO $ gitConfigRead g
gitAnnexChange g'
backendsAnnexChange $ parseBackendList $
gitConfig g' "annex.backends" ""
prepUUID
inBackend file yes no = do
r <- lookupFile file
r <- liftIO $ lookupFile file
case (r) of
Just v -> yes v
Nothing -> no
@ -48,13 +54,16 @@ notinBackend file yes no = inBackend file no yes
{- Annexes a file, storing it in a backend, and then moving it into
- the annex directory and setting up the symlink pointing to its content. -}
annexFile :: State -> FilePath -> IO ()
annexFile state file = inBackend file err $ do
checkLegal file
stored <- storeFile state file
annexFile :: FilePath -> Annex ()
annexFile file = inBackend file err $ do
liftIO $ checkLegal file
stored <- storeFile file
g <- gitAnnex
case (stored) of
Nothing -> error $ "no backend could store: " ++ file
Just (key, backend) -> setup key backend
Just (key, backend) -> do
logStatus key ValuePresent
liftIO $ setup g key backend
where
err = error $ "already annexed " ++ file
checkLegal file = do
@ -62,15 +71,14 @@ annexFile state file = inBackend file err $ do
if ((isSymbolicLink s) || (not $ isRegularFile s))
then error $ "not a regular file: " ++ file
else return ()
setup key backend = do
logStatus state key ValuePresent
let dest = annexLocation (repo state) backend key
let reldest = annexLocationRelative (repo state) backend key
setup g key backend = do
let dest = annexLocation g backend key
let reldest = annexLocationRelative g backend key
createDirectoryIfMissing True (parentDir dest)
renameFile file dest
createSymbolicLink ((linkTarget file) ++ reldest) file
gitRun (repo state) ["add", file]
gitRun (repo state) ["commit", "-m",
gitRun g ["add", file]
gitRun g ["commit", "-m",
("git-annex annexed " ++ file), file]
linkTarget file =
-- relies on file being relative to the top of the
@ -83,56 +91,60 @@ annexFile state file = inBackend file err $ do
{- Inverse of annexFile. -}
unannexFile :: State -> FilePath -> IO ()
unannexFile state file = notinBackend file err $ \(key, backend) -> do
dropFile state backend key
logStatus state key ValueMissing
removeFile file
gitRun (repo state) ["rm", file]
gitRun (repo state) ["commit", "-m",
("git-annex unannexed " ++ file), file]
-- git rm deletes empty directories;
-- put them back
createDirectoryIfMissing True (parentDir file)
let src = annexLocation (repo state) backend key
renameFile src file
return ()
unannexFile :: FilePath -> Annex ()
unannexFile file = notinBackend file err $ \(key, backend) -> do
dropFile backend key
logStatus key ValueMissing
g <- gitAnnex
let src = annexLocation g backend key
liftIO $ moveout g src
where
err = error $ "not annexed " ++ file
moveout g src = do
removeFile file
gitRun g ["rm", file]
gitRun g ["commit", "-m",
("git-annex unannexed " ++ file), file]
-- git rm deletes empty directories;
-- put them back
createDirectoryIfMissing True (parentDir file)
renameFile src file
return ()
{- Gets an annexed file from one of the backends. -}
annexGetFile :: State -> FilePath -> IO ()
annexGetFile state file = notinBackend file err $ \(key, backend) -> do
inannex <- inAnnex state backend key
annexGetFile :: FilePath -> Annex ()
annexGetFile file = notinBackend file err $ \(key, backend) -> do
inannex <- inAnnex backend key
if (inannex)
then return ()
else do
let dest = annexLocation (repo state) backend key
createDirectoryIfMissing True (parentDir dest)
success <- retrieveFile state backend key dest
g <- gitAnnex
let dest = annexLocation g backend key
liftIO $ createDirectoryIfMissing True (parentDir dest)
success <- retrieveFile backend key dest
if (success)
then do
logStatus state key ValuePresent
logStatus key ValuePresent
return ()
else error $ "failed to get " ++ file
where
err = error $ "not annexed " ++ file
{- Indicates a file is wanted. -}
annexWantFile :: State -> FilePath -> IO ()
annexWantFile state file = do error "not implemented" -- TODO
annexWantFile :: FilePath -> Annex ()
annexWantFile file = do error "not implemented" -- TODO
{- Indicates a file is not wanted. -}
annexDropFile :: State -> FilePath -> IO ()
annexDropFile state file = do error "not implemented" -- TODO
annexDropFile :: FilePath -> Annex ()
annexDropFile file = do error "not implemented" -- TODO
{- Pushes all files to a remote repository. -}
annexPushRepo :: State -> String -> IO ()
annexPushRepo state reponame = do error "not implemented" -- TODO
annexPushRepo :: String -> Annex ()
annexPushRepo reponame = do error "not implemented" -- TODO
{- Pulls all files from a remote repository. -}
annexPullRepo :: State -> String -> IO ()
annexPullRepo state reponame = do error "not implemented" -- TODO
annexPullRepo :: String -> Annex ()
annexPullRepo reponame = do error "not implemented" -- TODO
{- Sets up a git repo for git-annex. May be called repeatedly. -}
gitSetup :: GitRepo -> IO ()
@ -159,11 +171,19 @@ gitSetup repo = do
attributes]
{- Updates the LocationLog when a key's presence changes. -}
logStatus state key status = do
f <- logChange (repo state) key (getUUID state (repo state)) status
gitRun (repo state) ["add", f]
gitRun (repo state) ["commit", "-m", "git-annex log update", f]
logStatus :: Key -> LogStatus -> Annex ()
logStatus key status = do
g <- gitAnnex
u <- getUUID g
f <- liftIO $ logChange g key u status
liftIO $ commit g f
where
commit g f = do
gitRun g ["add", f]
gitRun g ["commit", "-m", "git-annex log update", f]
{- Checks if a given key is currently present in the annexLocation -}
inAnnex :: State -> Backend -> Key -> IO Bool
inAnnex state backend key = doesFileExist $ annexLocation (repo state) backend key
inAnnex :: Backend -> Key -> Annex Bool
inAnnex backend key = do
g <- gitAnnex
liftIO $ doesFileExist $ annexLocation g backend key

View file

@ -20,6 +20,7 @@ module Backend (
lookupFile
) where
import Control.Monad.State
import Control.Exception
import System.Directory
import System.FilePath
@ -32,30 +33,34 @@ import Utility
import Types
{- Attempts to store a file in one of the backends. -}
storeFile :: State -> FilePath -> IO (Maybe (Key, Backend))
storeFile state file = storeFile' (backends state) state file
storeFile :: FilePath -> Annex (Maybe (Key, Backend))
storeFile file = do
g <- gitAnnex
let relfile = gitRelative g file
b <- backendsAnnex
storeFile' b file relfile
storeFile' [] _ _ = return Nothing
storeFile' (b:bs) state file = do
try <- (getKey b) state (gitRelative (repo state) file)
storeFile' (b:bs) file relfile = do
try <- (getKey b) relfile
case (try) of
Nothing -> nextbackend
Just key -> do
stored <- (storeFileKey b) state file key
stored <- (storeFileKey b) file key
if (not stored)
then nextbackend
else do
return $ Just (key, b)
where
nextbackend = storeFile' bs state file
nextbackend = storeFile' bs file relfile
{- Attempts to retrieve an key from one of the backends, saving it to
- a specified location. -}
retrieveFile :: State -> Backend -> Key -> FilePath -> IO Bool
retrieveFile state backend key dest = (retrieveKeyFile backend) state key dest
retrieveFile :: Backend -> Key -> FilePath -> Annex Bool
retrieveFile backend key dest = (retrieveKeyFile backend) key dest
{- Drops a key from a backend. -}
dropFile :: State -> Backend -> Key -> IO Bool
dropFile state backend key = (removeKey backend) state key
dropFile :: Backend -> Key -> Annex Bool
dropFile backend key = (removeKey backend) key
{- Looks up the key and backend corresponding to an annexed file,
- by examining what the file symlinks to. -}

View file

@ -14,5 +14,5 @@ backend = BackendFile.backend {
}
-- checksum the file to get its key
keyValue :: State -> FilePath -> IO (Maybe Key)
keyValue :: FilePath -> Annex (Maybe Key)
keyValue k = error "checksum keyValue unimplemented" -- TODO

View file

@ -3,6 +3,7 @@
module BackendFile (backend) where
import Control.Monad.State
import System.IO
import System.Cmd
import Control.Exception
@ -21,28 +22,28 @@ backend = Backend {
}
-- direct mapping from filename to key
keyValue :: State -> FilePath -> IO (Maybe Key)
keyValue state file = return $ Just $ Key file
keyValue :: FilePath -> Annex (Maybe Key)
keyValue file = return $ Just $ Key file
{- This backend does not really do any independant data storage,
- it relies on the file contents in .git/annex/ in this repo,
- and other accessible repos. So storing or removing a key is
- a no-op. TODO until support is added for git annex --push otherrepo,
- then these could implement that.. -}
dummyStore :: State -> FilePath -> Key -> IO (Bool)
dummyStore state file key = return True
dummyRemove :: State -> Key -> IO Bool
dummyRemove state url = return False
dummyStore :: FilePath -> Key -> Annex (Bool)
dummyStore file key = return True
dummyRemove :: Key -> Annex Bool
dummyRemove url = return False
{- Try to find a copy of the file in one of the remotes,
- and copy it over to this one. -}
copyKeyFile :: State -> Key -> FilePath -> IO (Bool)
copyKeyFile state key file = do
remotes <- remotesWithKey state key
copyKeyFile :: Key -> FilePath -> Annex (Bool)
copyKeyFile key file = do
remotes <- remotesWithKey key
if (0 == length remotes)
then error $ "no known remotes have: " ++ (keyFile key) ++ "\n" ++
"(Perhaps you need to git remote add a repository?)"
else trycopy remotes remotes
else liftIO $ trycopy remotes remotes
where
trycopy full [] = error $ "unable to get: " ++ (keyFile key) ++ "\n" ++
"To get that file, need access to one of these remotes: " ++

View file

@ -3,6 +3,7 @@
module BackendUrl (backend) where
import Control.Monad.State
import System.Cmd
import IO
import Types
@ -16,19 +17,19 @@ backend = Backend {
}
-- cannot generate url from filename
keyValue :: State -> FilePath -> IO (Maybe Key)
keyValue repo file = return Nothing
keyValue :: FilePath -> Annex (Maybe Key)
keyValue file = return Nothing
-- cannot change url contents
dummyStore :: State -> FilePath -> Key -> IO Bool
dummyStore repo file url = return False
dummyRemove :: State -> Key -> IO Bool
dummyRemove state url = return False
dummyStore :: FilePath -> Key -> Annex Bool
dummyStore file url = return False
dummyRemove :: Key -> Annex Bool
dummyRemove url = return False
downloadUrl :: State -> Key -> FilePath -> IO Bool
downloadUrl state url file = do
putStrLn $ "download: " ++ (show url)
result <- try $ rawSystem "curl" ["-#", "-o", file, (show url)]
downloadUrl :: Key -> FilePath -> Annex Bool
downloadUrl url file = do
liftIO $ putStrLn $ "download: " ++ (show url)
result <- liftIO $ try $ rawSystem "curl" ["-#", "-o", file, (show url)]
case (result) of
Left _ -> return False
Right _ -> return True

View file

@ -6,7 +6,8 @@
module CmdLine (
argvToMode,
dispatch
dispatch,
Mode
) where
import System.Console.GetOpt
@ -39,13 +40,13 @@ argvToMode argv = do
(_,files,errs) -> ioError (userError (concat errs ++ usageInfo header options))
where header = "Usage: git-annex [mode] file"
dispatch :: State -> Mode -> FilePath -> IO ()
dispatch state mode item = do
dispatch :: Mode -> FilePath -> Annex ()
dispatch mode item = do
case (mode) of
Add -> annexFile state item
Push -> annexPushRepo state item
Pull -> annexPullRepo state item
Want -> annexWantFile state item
Get -> annexGetFile state item
Drop -> annexDropFile state item
Unannex -> unannexFile state item
Add -> annexFile item
Push -> annexPushRepo item
Pull -> annexPullRepo item
Want -> annexWantFile item
Get -> annexGetFile item
Drop -> annexDropFile item
Unannex -> unannexFile item

View file

@ -5,6 +5,7 @@ module Remotes (
remotesWithKey
) where
import Control.Monad.State (liftIO)
import Types
import GitRepo
import LocationLog
@ -17,34 +18,43 @@ remotesList :: [GitRepo] -> String
remotesList remotes = join " " $ map gitRepoDescribe remotes
{- Cost ordered list of remotes that the LocationLog indicate may have a key. -}
remotesWithKey :: State -> Key -> IO [GitRepo]
remotesWithKey state key = do
uuids <- keyLocations (repo state) key
return $ reposByUUID state (remotesByCost state) uuids
remotesWithKey :: Key -> Annex [GitRepo]
remotesWithKey key = do
g <- gitAnnex
uuids <- liftIO $ keyLocations g key
remotes <- remotesByCost
reposByUUID remotes uuids
{- Cost Ordered list of remotes. -}
remotesByCost :: State -> [GitRepo]
remotesByCost state = reposByCost state $ gitConfigRemotes (repo state)
remotesByCost :: Annex [GitRepo]
remotesByCost = do
g <- gitAnnex
reposByCost $ gitConfigRemotes g
{- Orders a list of git repos by cost. -}
reposByCost :: State -> [GitRepo] -> [GitRepo]
reposByCost state l =
fst $ unzip $ sortBy (\(r1, c1) (r2, c2) -> compare c1 c2) $ costpairs l
reposByCost :: [GitRepo] -> Annex [GitRepo]
reposByCost l = do
costpairs <- mapM costpair l
return $ fst $ unzip $ sortBy bycost $ costpairs
where
costpairs l = map (\r -> (r, repoCost state r)) l
costpair r = do
cost <- repoCost r
return (r, cost)
bycost (_, c1) (_, c2) = compare c1 c2
{- Calculates cost for a repo.
-
- The default cost is 100 for local repositories, and 200 for remote
- repositories; it can also be configured by remote.<name>.annex-cost
-}
repoCost :: State -> GitRepo -> Int
repoCost state r =
if ((length $ config state r) > 0)
then read $ config state r
repoCost :: GitRepo -> Annex Int
repoCost r = do
g <- gitAnnex
if ((length $ config g r) > 0)
then return $ read $ config g r
else if (gitRepoIsLocal r)
then 100
else 200
then return 100
else return 200
where
config state r = gitConfig (repo state) (configkey r) ""
config g r = gitConfig g (configkey r) ""
configkey r = "remote." ++ (gitRepoRemoteName r) ++ ".annex-cost"

4
TODO
View file

@ -1,9 +1,9 @@
* bug when annexing files while in a subdir of a git repo
* bug when specifying absolute path to files when annexing
* implement retrieval for backendfile
* state monad
* query remotes for their annex.uuid settings
* query remotes for their annex.uuid settings and cache
* --push/--pull/--want/--drop

View file

@ -1,20 +1,59 @@
{- git-annex core data types -}
module Types (
State(..),
Annex(..),
makeAnnexState,
runAnnexState,
gitAnnex,
gitAnnexChange,
backendsAnnex,
backendsAnnexChange,
AnnexState(..),
Key(..),
Backend(..)
) where
import Control.Monad.State
import Data.String.Utils
import GitRepo
-- git-annex's runtime state
data State = State {
data AnnexState = AnnexState {
repo :: GitRepo,
backends :: [Backend]
} deriving (Show)
-- git-annex's monad
type Annex = StateT AnnexState IO
-- constructor
makeAnnexState :: GitRepo -> AnnexState
makeAnnexState g = AnnexState { repo = g, backends = [] }
-- performs an action in the Annex monad
runAnnexState state action = runStateT (action) state
-- state accessors
gitAnnex :: Annex GitRepo
gitAnnex = do
state <- get
return (repo state)
gitAnnexChange :: GitRepo -> Annex ()
gitAnnexChange r = do
state <- get
put state { repo = r }
return ()
backendsAnnex :: Annex [Backend]
backendsAnnex = do
state <- get
return (backends state)
backendsAnnexChange :: [Backend] -> Annex ()
backendsAnnexChange b = do
state <- get
put state { backends = b }
return ()
-- annexed filenames are mapped into keys
data Key = Key String deriving (Eq)
@ -27,13 +66,13 @@ data Backend = Backend {
-- name of this backend
name :: String,
-- converts a filename to a key
getKey :: State -> FilePath -> IO (Maybe Key),
getKey :: FilePath -> Annex (Maybe Key),
-- stores a file's contents to a key
storeFileKey :: State -> FilePath -> Key -> IO Bool,
storeFileKey :: FilePath -> Key -> Annex Bool,
-- retrieves a key's contents to a file
retrieveKeyFile :: State -> Key -> FilePath -> IO Bool,
retrieveKeyFile :: Key -> FilePath -> Annex Bool,
-- removes a key
removeKey :: State -> Key -> IO Bool
removeKey :: Key -> Annex Bool
}
instance Show Backend where

50
UUID.hs
View file

@ -13,6 +13,7 @@ module UUID (
reposByUUID
) where
import Control.Monad.State
import Maybe
import List
import System.Cmd.Utils
@ -26,9 +27,8 @@ configkey="annex.uuid"
{- Generates a UUID. There is a library for this, but it's not packaged,
- so use the command line tool. -}
genUUID :: IO UUID
genUUID = do
pOpen ReadFromPipe "uuid" ["-m"] $ \h -> hGetLine h
genUUID :: Annex UUID
genUUID = liftIO $ pOpen ReadFromPipe "uuid" ["-m"] $ \h -> hGetLine h
{- Looks up a repo's UUID. May return "" if none is known.
-
@ -36,28 +36,38 @@ genUUID = do
- remote.<name>.annex-uuid
-
- -}
getUUID :: State -> GitRepo -> UUID
getUUID s r =
if ("" /= getUUID' r)
then getUUID' r
else cached s r
getUUID :: GitRepo -> Annex UUID
getUUID r = do
if ("" /= configured r)
then return $ configured r
else cached r
where
cached s r = gitConfig (repo s) (configkey r) ""
configured r = gitConfig r "annex.uuid" ""
cached r = do
g <- gitAnnex
return $ gitConfig g (configkey r) ""
configkey r = "remote." ++ (gitRepoRemoteName r) ++ ".annex-uuid"
getUUID' r = gitConfig r "annex.uuid" ""
{- Make sure that the repo has an annex.uuid setting. -}
prepUUID :: GitRepo -> IO GitRepo
prepUUID repo =
if ("" == getUUID' repo)
prepUUID :: Annex ()
prepUUID = do
g <- gitAnnex
u <- getUUID g
if ("" == u)
then do
uuid <- genUUID
gitRun repo ["config", configkey, uuid]
-- return new repo with updated config
gitConfigRead repo
else return repo
liftIO $ gitRun g ["config", configkey, uuid]
-- re-read git config and update the repo's state
u' <- liftIO $ gitConfigRead g
gitAnnexChange u'
return ()
else return ()
{- Filters a list of repos to ones that have listed UUIDs. -}
reposByUUID :: State -> [GitRepo] -> [UUID] -> [GitRepo]
reposByUUID state repos uuids =
filter (\r -> isJust $ elemIndex (getUUID state r) uuids) repos
reposByUUID :: [GitRepo] -> [UUID] -> Annex [GitRepo]
reposByUUID repos uuids = do
filterM match repos
where
match r = do
u <- getUUID r
return $ isJust $ elemIndex u uuids

View file

@ -1,36 +1,43 @@
{- git-annex main program
- -}
import Control.Monad.State
import System.IO
import System.Environment
import Control.Exception
import CmdLine
import Types
import Annex
main = do
args <- getArgs
(mode, files) <- argvToMode args
(mode, params) <- argvToMode args
state <- startAnnex
tryRun state mode 0 0 params
tryRun 0 0 $ map (\f -> dispatch state mode f) files
{- Tries to run a series of actions, not stopping if some error out,
- and propigating an overall error status at the end. -}
tryRun errnum oknum [] = do
{- Processes each param in the list by dispatching the handler function
- for the user-selection operation mode. Catches exceptions, not stopping
- if some error out, and propigates an overall error status at the end.
-
- This runs in the IO monad, not in the Annex monad. It seems that
- exceptions can only be caught in the IO monad, not in a stacked monad;
- or more likely I missed an easy way to do it. So, I have to laboriously
- thread AnnexState through this function.
-}
tryRun :: AnnexState -> Mode -> Int -> Int -> [String] -> IO ()
tryRun state mode errnum oknum [] = do
if (errnum > 0)
then error $ (show errnum) ++ " failed ; " ++ show (oknum) ++ " ok"
else return ()
tryRun errnum oknum (a:as) = do
result <- try (a)::IO (Either SomeException ())
tryRun state mode errnum oknum (f:fs) = do
result <- try (runAnnexState state (dispatch mode f))::IO (Either SomeException ((), AnnexState))
case (result) of
Left err -> do
showErr err
tryRun (errnum + 1) oknum as
Right _ -> tryRun errnum (oknum + 1) as
tryRun state mode (errnum + 1) oknum fs
Right (_,state') -> tryRun state' mode errnum (oknum + 1) fs
{- Exception pretty-printing. -}
showErr :: SomeException -> IO ()
showErr e = do
hPutStrLn stderr $ "git-annex: " ++ (show e)
return ()