summaryrefslogtreecommitdiff
path: root/GitRepo.hs
diff options
context:
space:
mode:
Diffstat (limited to 'GitRepo.hs')
-rw-r--r--GitRepo.hs267
1 files changed, 267 insertions, 0 deletions
diff --git a/GitRepo.hs b/GitRepo.hs
new file mode 100644
index 000000000..5b0e68cd6
--- /dev/null
+++ b/GitRepo.hs
@@ -0,0 +1,267 @@
+{- git repository handling
+ -
+ - This is written to be completely independant of git-annex and should be
+ - suitable for other uses.
+ -
+ -}
+
+module GitRepo (
+ Repo,
+ repoFromCwd,
+ repoFromPath,
+ repoFromUrl,
+ repoIsLocal,
+ repoIsRemote,
+ repoDescribe,
+ workTree,
+ dir,
+ relative,
+ configGet,
+ configMap,
+ configRead,
+ run,
+ pipeRead,
+ attributes,
+ remotes,
+ remotesAdd,
+ repoRemoteName,
+ inRepo,
+ notInRepo
+) where
+
+import Directory
+import System
+import System.Directory
+import System.Posix.Directory
+import System.Path
+import System.Cmd
+import System.Cmd.Utils
+import System.IO
+import IO (bracket_)
+import Data.String.Utils
+import Data.Map as Map hiding (map, split)
+import Network.URI
+import Maybe
+
+import Utility
+
+{- A git repository can be on local disk or remote. Not to be confused
+ - with a git repo's configured remotes, some of which may be on local
+ - disk. -}
+data Repo =
+ LocalRepo {
+ top :: FilePath,
+ config :: Map String String,
+ remotes :: [Repo],
+ -- remoteName holds the name used for this repo in remotes
+ remoteName :: Maybe String
+ } | RemoteRepo {
+ url :: String,
+ top :: FilePath,
+ config :: Map String String,
+ remotes :: [Repo],
+ remoteName :: Maybe String
+ } deriving (Show, Read, Eq)
+
+{- Local Repo constructor. -}
+repoFromPath :: FilePath -> Repo
+repoFromPath dir =
+ LocalRepo {
+ top = dir,
+ config = Map.empty,
+ remotes = [],
+ remoteName = Nothing
+ }
+
+{- Remote Repo constructor. Throws exception on invalid url. -}
+repoFromUrl :: String -> Repo
+repoFromUrl url =
+ RemoteRepo {
+ url = url,
+ top = path url,
+ config = Map.empty,
+ remotes = [],
+ remoteName = Nothing
+ }
+ where path url = uriPath $ fromJust $ parseURI url
+
+{- User-visible description of a git repo. -}
+repoDescribe repo =
+ if (isJust $ remoteName repo)
+ then fromJust $ remoteName repo
+ else if (repoIsLocal repo)
+ then top repo
+ else url repo
+
+{- Constructs and returns an updated version of a repo with
+ - different remotes list. -}
+remotesAdd :: Repo -> [Repo] -> Repo
+remotesAdd repo rs = repo { remotes = rs }
+
+{- Returns the name of the remote that corresponds to the repo, if
+ - it is a remote. Otherwise, "" -}
+repoRemoteName r =
+ if (isJust $ remoteName r)
+ then fromJust $ remoteName r
+ else ""
+
+{- Some code needs to vary between remote and local repos, or bare and
+ - non-bare, these functions help with that. -}
+repoIsLocal repo = case (repo) of
+ LocalRepo {} -> True
+ RemoteRepo {} -> False
+repoIsRemote repo = not $ repoIsLocal repo
+assertlocal repo action =
+ if (repoIsLocal repo)
+ then action
+ else error $ "acting on remote git repo " ++ (repoDescribe repo) ++
+ " not supported"
+bare :: Repo -> Bool
+bare repo =
+ if (member b (config repo))
+ then ("true" == fromJust (Map.lookup b (config repo)))
+ else error $ "it is not known if git repo " ++ (repoDescribe repo) ++
+ " is a bare repository; config not read"
+ where
+ b = "core.bare"
+
+{- Path to a repository's gitattributes file. -}
+attributes :: Repo -> String
+attributes repo = assertlocal repo $ do
+ if (bare repo)
+ then (top repo) ++ "/info/.gitattributes"
+ else (top repo) ++ "/.gitattributes"
+
+{- Path to a repository's .git directory, relative to its topdir. -}
+dir :: Repo -> String
+dir repo = assertlocal repo $
+ if (bare repo)
+ then ""
+ else ".git"
+
+{- Path to a repository's --work-tree. -}
+workTree :: Repo -> FilePath
+workTree repo = top repo
+
+{- Given a relative or absolute filename in a repository, calculates the
+ - name to use to refer to the file relative to a git repository's top.
+ - This is the same form displayed and used by git. -}
+relative :: Repo -> String -> String
+relative 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
+
+{- Constructs a git command line operating on the specified repo. -}
+gitCommandLine :: Repo -> [String] -> [String]
+gitCommandLine repo params = assertlocal repo $
+ -- force use of specified repo via --git-dir and --work-tree
+ ["--git-dir="++(top repo)++"/"++(dir repo), "--work-tree="++(top repo)] ++ params
+
+{- Runs git in the specified repo. -}
+run :: Repo -> [String] -> IO ()
+run repo params = assertlocal repo $ do
+ r <- safeSystem "git" (gitCommandLine repo params)
+ return ()
+
+{- Runs a git subcommand and returns its output. -}
+pipeRead :: Repo -> [String] -> IO String
+pipeRead repo params = assertlocal repo $ do
+ pOpen ReadFromPipe "git" (gitCommandLine repo params) $ \h -> do
+ ret <- hGetContentsStrict h
+ return ret
+
+{- Passed a location, recursively scans for all files that
+ - are checked into git at that location. -}
+inRepo :: Repo -> FilePath -> IO [FilePath]
+inRepo repo location = do
+ s <- pipeRead repo ["ls-files", "--cached", "--exclude-standard", location]
+ return $ lines s
+
+{- Passed a location, recursively scans for all files that are not checked
+ - into git, and not gitignored. -}
+notInRepo :: Repo -> FilePath -> IO [FilePath]
+notInRepo repo location = do
+ s <- pipeRead repo ["ls-files", "--others", "--exclude-standard", location]
+ return $ lines s
+
+{- Runs git config and populates a repo with its config. -}
+configRead :: Repo -> IO Repo
+configRead repo = assertlocal repo $ do
+ {- Cannot use pipeRead because it relies on the config having
+ been already read. Instead, chdir to the repo. -}
+ cwd <- getCurrentDirectory
+ bracket_ (changeWorkingDirectory (top repo))
+ (\_ -> changeWorkingDirectory cwd) $
+ pOpen ReadFromPipe "git" ["config", "--list"] $ \h -> do
+ val <- hGetContentsStrict h
+ let r = repo { config = configParse val }
+ return r { remotes = configRemotes r }
+
+{- Calculates a list of a repo's configured remotes, by parsing its config. -}
+configRemotes :: Repo -> [Repo]
+configRemotes repo = map construct remotes
+ where
+ remotes = toList $ filter $ config repo
+ filter = filterWithKey (\k _ -> isremote k)
+ isremote k = (startswith "remote." k) && (endswith ".url" k)
+ remotename k = (split "." k) !! 1
+ construct (k,v) = (gen v) { remoteName = Just $ remotename k }
+ gen v = if (isURI v)
+ then repoFromUrl v
+ else repoFromPath v
+
+{- Parses git config --list output into a config map. -}
+configParse :: String -> Map.Map String String
+configParse s = Map.fromList $ map pair $ lines s
+ where
+ pair l = (key l, val l)
+ key l = (keyval l) !! 0
+ val l = join sep $ drop 1 $ keyval l
+ keyval l = split sep l :: [String]
+ sep = "="
+
+{- Returns a single git config setting, or a default value if not set. -}
+configGet :: Repo -> String -> String -> String
+configGet repo key defaultValue =
+ Map.findWithDefault defaultValue key (config repo)
+
+{- Access to raw config Map -}
+configMap :: Repo -> Map String String
+configMap repo = config repo
+
+{- Finds the current git repository, which may be in a parent directory. -}
+repoFromCwd :: IO Repo
+repoFromCwd = do
+ cwd <- getCurrentDirectory
+ top <- seekUp cwd isRepoTop
+ case top of
+ (Just dir) -> return $ repoFromPath 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 <- isRepo dir
+ b <- isBareRepo dir
+ return (r || b)
+ where
+ isRepo 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)