239 lines
6.8 KiB
Haskell
239 lines
6.8 KiB
Haskell
{- management of the git-annex branch
|
|
-
|
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
|
-
|
|
- Licensed under the GNU GPL version 3 or higher.
|
|
-}
|
|
|
|
module Branch (
|
|
create,
|
|
update,
|
|
get,
|
|
change,
|
|
commit,
|
|
files
|
|
) where
|
|
|
|
import Control.Monad (unless, when, liftM)
|
|
import Control.Monad.State (liftIO)
|
|
import System.FilePath
|
|
import System.Directory
|
|
import Data.String.Utils
|
|
import System.Cmd.Utils
|
|
import Data.Maybe
|
|
import Data.List
|
|
import System.IO
|
|
import System.Posix.IO
|
|
import System.Posix.Process
|
|
|
|
import Types.BranchState
|
|
import qualified GitRepo as Git
|
|
import qualified GitUnionMerge
|
|
import qualified Annex
|
|
import Utility
|
|
import Types
|
|
import Messages
|
|
|
|
{- Name of the branch that is used to store git-annex's information. -}
|
|
name :: String
|
|
name = "git-annex"
|
|
|
|
{- Fully qualified name of the branch. -}
|
|
fullname :: String
|
|
fullname = "refs/heads/" ++ name
|
|
|
|
shortref :: String -> String
|
|
shortref = remove "refs/heads/" . remove "refs/remotes/"
|
|
where
|
|
remove prefix s
|
|
| prefix `isPrefixOf` s = drop (length prefix) s
|
|
| otherwise = s
|
|
|
|
{- A separate index file for the branch. -}
|
|
index :: Git.Repo -> FilePath
|
|
index g = Git.workTree g </> Git.gitDir g </> "index." ++ name
|
|
|
|
{- Populates the branch's index file with the current branch contents.
|
|
-
|
|
- Usually, this is only done when the index doesn't yet exist, and
|
|
- the index is used to build up changes to be commited to the branch.
|
|
-}
|
|
genIndex :: Git.Repo -> IO ()
|
|
genIndex g = do
|
|
ls <- Git.pipeNullSplit g $
|
|
map Param ["ls-tree", "-z", "-r", "--full-tree", fullname]
|
|
forceSuccess =<< Git.pipeWrite g
|
|
(map Param ["update-index", "-z", "--index-info"])
|
|
(join "\0" ls)
|
|
|
|
{- Runs an action using the branch's index file. -}
|
|
withIndex :: Annex a -> Annex a
|
|
withIndex = withIndex' False
|
|
withIndex' :: Bool -> Annex a -> Annex a
|
|
withIndex' bootstrapping a = do
|
|
g <- Annex.gitRepo
|
|
let f = index g
|
|
reset <- liftIO $ Git.useIndex f
|
|
|
|
unless bootstrapping $ do
|
|
e <- liftIO $ doesFileExist f
|
|
unless e $ liftIO $ genIndex g
|
|
|
|
r <- a
|
|
liftIO reset
|
|
return r
|
|
|
|
withIndexUpdate :: Annex a -> Annex a
|
|
withIndexUpdate a = update >> withIndex a
|
|
|
|
getState :: Annex BranchState
|
|
getState = Annex.getState Annex.branchstate
|
|
|
|
setState :: BranchState -> Annex ()
|
|
setState state = Annex.changeState $ \s -> s { Annex.branchstate = state }
|
|
|
|
setCache :: FilePath -> String -> Annex ()
|
|
setCache file content = do
|
|
state <- getState
|
|
setState state { cachedFile = Just file, cachedContent = content }
|
|
|
|
setCacheChanged :: FilePath -> String -> Annex ()
|
|
setCacheChanged file content = do
|
|
state <- getState
|
|
setState state { cachedFile = Just file, cachedContent = content, branchChanged = True }
|
|
|
|
invalidateCache :: Annex ()
|
|
invalidateCache = do
|
|
state <- getState
|
|
setState state { cachedFile = Nothing, cachedContent = "" }
|
|
|
|
getCache :: FilePath -> Annex (Maybe String)
|
|
getCache file = getState >>= handle
|
|
where
|
|
handle state
|
|
| cachedFile state == Just file =
|
|
return $ Just $ cachedContent state
|
|
| otherwise = return Nothing
|
|
|
|
{- Creates the branch, if it does not already exist. -}
|
|
create :: Annex ()
|
|
create = do
|
|
exists <- refexists fullname
|
|
unless exists $ do
|
|
g <- Annex.gitRepo
|
|
inorigin <- refexists origin
|
|
if inorigin
|
|
then liftIO $ Git.run g "branch" [Param name, Param origin]
|
|
else withIndex' True $
|
|
liftIO $ GitUnionMerge.commit g "branch created" fullname []
|
|
where
|
|
origin = "origin/" ++ name
|
|
refexists ref = do
|
|
g <- Annex.gitRepo
|
|
liftIO $ Git.runBool g "show-ref"
|
|
[Param "--verify", Param "-q", Param ref]
|
|
|
|
{- Commits any staged changes to the branch. -}
|
|
commit :: String -> Annex ()
|
|
commit message = do
|
|
state <- getState
|
|
when (branchChanged state) $ do
|
|
g <- Annex.gitRepo
|
|
withIndex $ liftIO $
|
|
GitUnionMerge.commit g message fullname [fullname]
|
|
|
|
{- Ensures that the branch is up-to-date; should be called before
|
|
- data is read from it. Runs only once per git-annex run. -}
|
|
update :: Annex ()
|
|
update = do
|
|
state <- Annex.getState Annex.branchstate
|
|
unless (branchUpdated state) $ withIndex $ do
|
|
g <- Annex.gitRepo
|
|
r <- liftIO $ Git.pipeRead g [Param "show-ref", Param name]
|
|
let refs = map (last . words) (lines r)
|
|
updated <- catMaybes `liftM` mapM updateRef refs
|
|
unless (null updated) $ liftIO $
|
|
GitUnionMerge.commit g "update" fullname
|
|
(fullname:updated)
|
|
Annex.changeState $ \s -> s { Annex.branchstate = state { branchUpdated = True } }
|
|
invalidateCache
|
|
|
|
{- Ensures that a given ref has been merged into the index. -}
|
|
updateRef :: String -> Annex (Maybe String)
|
|
updateRef ref
|
|
| ref == fullname = return Nothing
|
|
| otherwise = do
|
|
g <- Annex.gitRepo
|
|
-- checking with log to see if there have been changes
|
|
-- is less expensive than always merging
|
|
diffs <- liftIO $ Git.pipeRead g [
|
|
Param "log",
|
|
Param (name++".."++ref),
|
|
Params "--oneline -n1"
|
|
]
|
|
if (null diffs)
|
|
then return Nothing
|
|
else do
|
|
showSideAction $ "merging " ++ shortref ref ++ " into " ++ name ++ "..."
|
|
-- By passing only one ref, it is actually
|
|
-- merged into the index, preserving any
|
|
-- changes that may already be staged.
|
|
--
|
|
-- However, any changes in the git-annex
|
|
-- branch that are *not* reflected in the
|
|
-- index will be removed. So, documentation
|
|
-- advises users not to directly modify the
|
|
-- branch.
|
|
liftIO $ GitUnionMerge.merge g [ref]
|
|
return $ Just ref
|
|
|
|
{- Stages the content of a file into the branch's index. -}
|
|
change :: FilePath -> String -> Annex ()
|
|
change file content = do
|
|
g <- Annex.gitRepo
|
|
sha <- liftIO $ Git.hashObject g content
|
|
withIndex $ liftIO $ Git.run g "update-index"
|
|
[ Param "--add", Param "--cacheinfo", Param "100644",
|
|
Param sha, File file]
|
|
setCacheChanged file content
|
|
|
|
{- Gets the content of a file on the branch, or content staged in the index
|
|
- if it's newer. Returns an empty string if the file didn't exist yet. -}
|
|
get :: FilePath -> Annex String
|
|
get file = do
|
|
cached <- getCache file
|
|
case cached of
|
|
Just content -> return content
|
|
Nothing -> withIndexUpdate $ do
|
|
g <- Annex.gitRepo
|
|
content <- liftIO $ catch (cat g) (const $ return "")
|
|
setCache file content
|
|
return content
|
|
where
|
|
cat g = cmdOutput "git" $ toCommand $ Git.gitCommandLine g
|
|
[Param "cat-file", Param "blob", Param $ ':':file]
|
|
|
|
{- Runs a command, returning its output, ignoring nonzero exit
|
|
- status, and discarding stderr. -}
|
|
cmdOutput :: FilePath -> [String] -> IO String
|
|
cmdOutput cmd params = do
|
|
pipepair <- createPipe
|
|
let callfunc _ = do
|
|
closeFd (snd pipepair)
|
|
h <- fdToHandle (fst pipepair)
|
|
x <- hGetContentsStrict h
|
|
hClose h
|
|
return $! x
|
|
pid <- pOpen3Raw Nothing (Just (snd pipepair)) Nothing cmd params
|
|
(closeFd (fst pipepair) >> closeFd stdError)
|
|
retval <- callfunc $! pid
|
|
let rv = seq retval retval
|
|
_ <- getProcessStatus True False pid
|
|
return rv
|
|
|
|
{- Lists all files on the branch. -}
|
|
files :: Annex [FilePath]
|
|
files = withIndexUpdate $ do
|
|
g <- Annex.gitRepo
|
|
liftIO $ Git.pipeNullSplit g
|
|
[Params "ls-tree --name-only -r -z", Param fullname]
|