{- git repository handling - - This is written to be completely independant of git-annex and should be - suitable for other uses. - - -} module GitRepo ( GitRepo, gitRepoCurrent, gitRepoTop, gitDir, gitRelative, gitConfig, gitAdd, gitAttributes ) where import Directory import System import System.Directory import System.Path import System.Cmd.Utils import System.IO import System.Posix.Process import Data.String.Utils import Data.Map as Map (fromList, empty, lookup, Map) import Utility -- a git repository data GitRepo = GitRepo { top :: FilePath, bare :: Bool, config :: Map String String } deriving (Show, Read, Eq) {- GitRepo constructor -} gitRepo :: FilePath -> IO GitRepo gitRepo dir = do b <- isBareRepo dir let r = GitRepo { top = dir, bare = b, config = Map.empty } r' <- gitConfigRead r return r' {- Field accessor. -} gitRepoTop :: GitRepo -> FilePath gitRepoTop repo = top repo {- Path to a repository's gitattributes file. -} gitAttributes :: GitRepo -> IO String gitAttributes repo = do if (bare repo) then return $ (top repo) ++ "/info/.gitattributes" else return $ (top repo) ++ "/.gitattributes" {- Path to a repository's .git directory. - (For a bare repository, that is the root of the repository.) - TODO: support GIT_DIR -} gitDir :: GitRepo -> String gitDir repo = if (bare repo) then top repo else top repo ++ "/.git" {- Given a relative or absolute filename, calculates the name to use - to refer to the file relative to a git repository directory. - This is the same form displayed and used by git. -} gitRelative :: GitRepo -> String -> String gitRelative repo file = drop (length absrepo) absfile where -- normalize both repo and file, so that repo -- will be substring of file absrepo = case (absNormPath "/" (top repo)) of Just f -> f ++ "/" Nothing -> error $ "bad repo" ++ (top repo) absfile = case (secureAbsNormPath absrepo file) of Just f -> f Nothing -> error $ file ++ " is not located inside git repository " ++ absrepo {- Stages a changed file in git's index. -} gitAdd :: GitRepo -> FilePath -> IO () gitAdd repo file = runGit repo ["add", file] {- Constructs a git command line operating on the specified repo. -} gitCommandLine :: GitRepo -> [String] -> [String] gitCommandLine repo params = -- force use of specified repo via --git-dir and --work-tree ["--git-dir="++(gitDir repo), "--work-tree="++(top repo)] ++ params {- Runs git in the specified repo. -} runGit :: GitRepo -> [String] -> IO () runGit repo params = do r <- executeFile "git" True (gitCommandLine repo params) Nothing return () {- Runs a git subcommand and returns its output. -} gitPipeRead :: GitRepo -> [String] -> IO String gitPipeRead repo params = pOpen ReadFromPipe "git" (gitCommandLine repo params) $ \h -> do ret <- hGetContentsStrict h return ret {- Runs git config and populates a repo with its settings. -} gitConfigRead :: GitRepo -> IO GitRepo gitConfigRead repo = do c <- gitPipeRead repo ["config", "--list"] return repo { config = Map.fromList $ parse c } where parse s = map ( \l -> (key l, val l) ) $ lines s keyval l = split sep l :: [String] key l = (keyval l) !! 0 val l = join sep $ drop 1 $ keyval l sep = "=" {- Returns a single git config setting, or a default value if not set. -} gitConfig :: GitRepo -> String -> String -> String gitConfig repo key defaultValue = case (Map.lookup key $ config repo) of Just value -> value Nothing -> defaultValue {- Finds the current git repository, which may be in a parent directory. -} gitRepoCurrent :: IO GitRepo gitRepoCurrent = do cwd <- getCurrentDirectory top <- seekUp cwd isRepoTop case top of (Just dir) -> gitRepo dir Nothing -> error "Not in a git repository." seekUp :: String -> (String -> IO Bool) -> IO (Maybe String) seekUp dir want = do ok <- want dir if ok then return (Just dir) else case (parentDir dir) of "" -> return Nothing d -> seekUp d want isRepoTop dir = do r <- isGitRepo dir b <- isBareRepo dir return (r || b) isGitRepo dir = gitSignature dir ".git" ".git/config" isBareRepo dir = gitSignature dir "objects" "config" gitSignature dir subdir file = do s <- (doesDirectoryExist (dir ++ "/" ++ subdir)) f <- (doesFileExist (dir ++ "/" ++ file)) return (s && f)