359 lines
		
	
	
	
		
			11 KiB
			
		
	
	
	
		
			Haskell
		
	
	
	
	
	
			
		
		
	
	
			359 lines
		
	
	
	
		
			11 KiB
			
		
	
	
	
		
			Haskell
		
	
	
	
	
	
{- management of the git-annex branch
 | 
						|
 -
 | 
						|
 - Copyright 2011-2012 Joey Hess <joey@kitenet.net>
 | 
						|
 -
 | 
						|
 - Licensed under the GNU GPL version 3 or higher.
 | 
						|
 -}
 | 
						|
 | 
						|
module Annex.Branch (
 | 
						|
	fullname,
 | 
						|
	name,
 | 
						|
	hasOrigin,
 | 
						|
	hasSibling,
 | 
						|
	siblingBranches,
 | 
						|
	create,
 | 
						|
	update,
 | 
						|
	forceUpdate,
 | 
						|
	updateTo,
 | 
						|
	get,
 | 
						|
	change,
 | 
						|
	commit,
 | 
						|
	stage,
 | 
						|
	files,
 | 
						|
) where
 | 
						|
 | 
						|
import qualified Data.ByteString.Lazy.Char8 as L
 | 
						|
 | 
						|
import Common.Annex
 | 
						|
import Annex.BranchState
 | 
						|
import Annex.Journal
 | 
						|
import qualified Git
 | 
						|
import qualified Git.Command
 | 
						|
import qualified Git.Ref
 | 
						|
import qualified Git.Branch
 | 
						|
import qualified Git.UnionMerge
 | 
						|
import qualified Git.UpdateIndex
 | 
						|
import Git.HashObject
 | 
						|
import Git.Types
 | 
						|
import Git.FilePath
 | 
						|
import Annex.CatFile
 | 
						|
import Annex.Perms
 | 
						|
import qualified Annex
 | 
						|
 | 
						|
{- Name of the branch that is used to store git-annex's information. -}
 | 
						|
name :: Git.Ref
 | 
						|
name = Git.Ref "git-annex"
 | 
						|
 | 
						|
{- Fully qualified name of the branch. -}
 | 
						|
fullname :: Git.Ref
 | 
						|
fullname = Git.Ref $ "refs/heads/" ++ show name
 | 
						|
 | 
						|
{- Branch's name in origin. -}
 | 
						|
originname :: Git.Ref
 | 
						|
originname = Git.Ref $ "origin/" ++ show name
 | 
						|
 | 
						|
{- Does origin/git-annex exist? -}
 | 
						|
hasOrigin :: Annex Bool
 | 
						|
hasOrigin = inRepo $ Git.Ref.exists originname
 | 
						|
 | 
						|
{- Does the git-annex branch or a sibling foo/git-annex branch exist? -}
 | 
						|
hasSibling :: Annex Bool
 | 
						|
hasSibling = not . null <$> siblingBranches
 | 
						|
 | 
						|
{- List of git-annex (refs, branches), including the main one and any
 | 
						|
 - from remotes. Duplicate refs are filtered out. -}
 | 
						|
siblingBranches :: Annex [(Git.Ref, Git.Branch)]
 | 
						|
siblingBranches = inRepo $ Git.Ref.matchingUniq name
 | 
						|
 | 
						|
{- Creates the branch, if it does not already exist. -}
 | 
						|
create :: Annex ()
 | 
						|
create = void getBranch
 | 
						|
 | 
						|
{- Returns the ref of the branch, creating it first if necessary. -}
 | 
						|
getBranch :: Annex Git.Ref
 | 
						|
getBranch = maybe (hasOrigin >>= go >>= use) return =<< branchsha
 | 
						|
	where
 | 
						|
		go True = do
 | 
						|
			inRepo $ Git.Command.run "branch"
 | 
						|
				[Param $ show name, Param $ show originname]
 | 
						|
			fromMaybe (error $ "failed to create " ++ show name)
 | 
						|
				<$> branchsha
 | 
						|
		go False = withIndex' True $
 | 
						|
			inRepo $ Git.Branch.commit "branch created" fullname []
 | 
						|
		use sha = do
 | 
						|
			setIndexSha sha
 | 
						|
			return sha
 | 
						|
		branchsha = inRepo $ Git.Ref.sha fullname
 | 
						|
 | 
						|
{- Ensures that the branch and index are is up-to-date; should be
 | 
						|
 - called before data is read from it. Runs only once per git-annex run.
 | 
						|
 -}
 | 
						|
update :: Annex ()
 | 
						|
update = runUpdateOnce $ void $ updateTo =<< siblingBranches
 | 
						|
 | 
						|
{- Forces an update even if one has already been run. -}
 | 
						|
forceUpdate :: Annex Bool
 | 
						|
forceUpdate = updateTo =<< siblingBranches
 | 
						|
 | 
						|
{- Merges the specified Refs into the index, if they have any changes not
 | 
						|
 - already in it. The Branch names are only used in the commit message;
 | 
						|
 - it's even possible that the provided Branches have not been updated to
 | 
						|
 - point to the Refs yet.
 | 
						|
 -
 | 
						|
 - Before refs are merged into the index, it's important to first stage the
 | 
						|
 - journal into the index. Otherwise, any changes in the journal would
 | 
						|
 - later get staged, and might overwrite changes made during the merge.
 | 
						|
 - If no Refs are provided, the journal is still staged and committed.
 | 
						|
 -
 | 
						|
 - (It would be cleaner to handle the merge by updating the journal, not the
 | 
						|
 - index, with changes from the branches.)
 | 
						|
 -
 | 
						|
 - The branch is fast-forwarded if possible, otherwise a merge commit is
 | 
						|
 - made.
 | 
						|
 -
 | 
						|
 - Returns True if any refs were merged in, False otherwise.
 | 
						|
 -}
 | 
						|
updateTo :: [(Git.Ref, Git.Branch)] -> Annex Bool
 | 
						|
updateTo pairs = do
 | 
						|
	-- ensure branch exists, and get its current ref
 | 
						|
	branchref <- getBranch
 | 
						|
	-- check what needs updating before taking the lock
 | 
						|
	dirty <- unCommitted
 | 
						|
	(refs, branches) <- unzip <$> filterM isnewer pairs
 | 
						|
	if not dirty && null refs
 | 
						|
		then updateIndex branchref
 | 
						|
		else withIndex $ lockJournal $ do
 | 
						|
			when dirty stageJournal
 | 
						|
			let merge_desc = if null branches
 | 
						|
				then "update"
 | 
						|
				else "merging " ++
 | 
						|
					unwords (map Git.Ref.describe branches) ++ 
 | 
						|
					" into " ++ show name
 | 
						|
			unless (null branches) $ do
 | 
						|
				showSideAction merge_desc
 | 
						|
				mergeIndex refs
 | 
						|
			ff <- if dirty
 | 
						|
				then return False
 | 
						|
				else inRepo $ Git.Branch.fastForward fullname refs
 | 
						|
			if ff
 | 
						|
				then updateIndex branchref
 | 
						|
				else commitBranch branchref merge_desc
 | 
						|
					(nub $ fullname:refs)
 | 
						|
			invalidateCache
 | 
						|
	return $ not $ null refs
 | 
						|
	where
 | 
						|
		isnewer (r, _) = inRepo $ Git.Branch.changed fullname r
 | 
						|
 | 
						|
{- Gets the content of a file on the branch, or content from the journal, or
 | 
						|
 - staged in the index.
 | 
						|
 -
 | 
						|
 - Returns an empty string if the file doesn't exist yet. -}
 | 
						|
get :: FilePath -> Annex String
 | 
						|
get = get' False
 | 
						|
 | 
						|
{- Like get, but does not merge the branch, so the info returned may not
 | 
						|
 - reflect changes in remotes. (Changing the value this returns, and then
 | 
						|
 - merging is always the same as using get, and then changing its value.) -}
 | 
						|
getStale :: FilePath -> Annex String
 | 
						|
getStale = get' True
 | 
						|
 | 
						|
get' :: Bool -> FilePath -> Annex String
 | 
						|
get' staleok file = fromcache =<< getCache file
 | 
						|
	where
 | 
						|
		fromcache (Just content) = return content
 | 
						|
		fromcache Nothing = fromjournal =<< getJournalFile file
 | 
						|
		fromjournal (Just content) = cache content
 | 
						|
		fromjournal Nothing
 | 
						|
			| staleok = withIndex frombranch
 | 
						|
			| otherwise = withIndexUpdate $ frombranch >>= cache
 | 
						|
		frombranch = L.unpack <$> catFile fullname file
 | 
						|
		cache content = do
 | 
						|
			setCache file content
 | 
						|
			return content
 | 
						|
 | 
						|
{- Applies a function to modifiy the content of a file.
 | 
						|
 -
 | 
						|
 - Note that this does not cause the branch to be merged, it only
 | 
						|
 - modifes the current content of the file on the branch.
 | 
						|
 -}
 | 
						|
change :: FilePath -> (String -> String) -> Annex ()
 | 
						|
change file a = lockJournal $ a <$> getStale file >>= set file
 | 
						|
 | 
						|
{- Records new content of a file into the journal and cache. -}
 | 
						|
set :: FilePath -> String -> Annex ()
 | 
						|
set file content = do
 | 
						|
	setJournalFile file content
 | 
						|
	setCache file content
 | 
						|
 | 
						|
{- Stages the journal, and commits staged changes to the branch. -}
 | 
						|
commit :: String -> Annex ()
 | 
						|
commit message = whenM unCommitted $ lockJournal $ do
 | 
						|
	stageJournal
 | 
						|
	ref <- getBranch
 | 
						|
	withIndex $ commitBranch ref message [fullname]
 | 
						|
 | 
						|
{- Stages the journal, not making a commit to the branch. -}
 | 
						|
stage :: Annex ()
 | 
						|
stage = whenM journalDirty $ lockJournal $ do
 | 
						|
	stageJournal
 | 
						|
	setUnCommitted
 | 
						|
 | 
						|
{- Commits the staged changes in the index to the branch.
 | 
						|
 - 
 | 
						|
 - Ensures that the branch's index file is first updated to the state
 | 
						|
 - of the branch at branchref, before running the commit action. This
 | 
						|
 - is needed because the branch may have had changes pushed to it, that
 | 
						|
 - are not yet reflected in the index.
 | 
						|
 -
 | 
						|
 - Also safely handles a race that can occur if a change is being pushed
 | 
						|
 - into the branch at the same time. When the race happens, the commit will
 | 
						|
 - be made on top of the newly pushed change, but without the index file
 | 
						|
 - being updated to include it. The result is that the newly pushed
 | 
						|
 - change is reverted. This race is detected and another commit made
 | 
						|
 - to fix it.
 | 
						|
 - 
 | 
						|
 - The branchref value can have been obtained using getBranch at any
 | 
						|
 - previous point, though getting it a long time ago makes the race
 | 
						|
 - more likely to occur.
 | 
						|
 -}
 | 
						|
commitBranch :: Git.Ref -> String -> [Git.Ref] -> Annex ()
 | 
						|
commitBranch branchref message parents = do
 | 
						|
	updateIndex branchref
 | 
						|
	committedref <- inRepo $ Git.Branch.commit message fullname parents
 | 
						|
	setIndexSha committedref
 | 
						|
	parentrefs <- commitparents <$> catObject committedref
 | 
						|
	when (racedetected branchref parentrefs) $
 | 
						|
		fixrace committedref parentrefs
 | 
						|
	setCommitted
 | 
						|
	where
 | 
						|
		-- look for "parent ref" lines and return the refs
 | 
						|
		commitparents = map (Git.Ref . snd) . filter isparent .
 | 
						|
			map (toassoc . L.unpack) . L.lines
 | 
						|
		toassoc = separate (== ' ')
 | 
						|
		isparent (k,_) = k == "parent"
 | 
						|
		
 | 
						|
 		{- The race can be detected by checking the commit's
 | 
						|
		 - parent, which will be the newly pushed branch,
 | 
						|
		 - instead of the expected ref that the index was updated to. -}
 | 
						|
		racedetected expectedref parentrefs
 | 
						|
			| expectedref `elem` parentrefs = False -- good parent
 | 
						|
			| otherwise = True -- race!
 | 
						|
		
 | 
						|
		{- To recover from the race, union merge the lost refs
 | 
						|
		 - into the index, and recommit on top of the bad commit. -}
 | 
						|
		fixrace committedref lostrefs = do
 | 
						|
			mergeIndex lostrefs
 | 
						|
			commitBranch committedref racemessage [committedref]
 | 
						|
		
 | 
						|
		racemessage = message ++ " (recovery from race)"
 | 
						|
 | 
						|
{- Lists all files on the branch. There may be duplicates in the list. -}
 | 
						|
files :: Annex [FilePath]
 | 
						|
files = withIndexUpdate $ do
 | 
						|
	bfiles <- inRepo $ Git.Command.pipeNullSplit
 | 
						|
		[Params "ls-tree --name-only -r -z", Param $ show fullname]
 | 
						|
	jfiles <- getJournalledFiles
 | 
						|
	return $ jfiles ++ bfiles
 | 
						|
 | 
						|
 | 
						|
{- Populates the branch's index file with the current branch contents.
 | 
						|
 - 
 | 
						|
 - 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, and merge
 | 
						|
 - in changes from other branches.
 | 
						|
 -}
 | 
						|
genIndex :: Git.Repo -> IO ()
 | 
						|
genIndex g = Git.UpdateIndex.streamUpdateIndex g
 | 
						|
	[Git.UpdateIndex.lsTree fullname g]
 | 
						|
 | 
						|
{- Merges the specified refs into the index.
 | 
						|
 - Any changes staged in the index will be preserved. -}
 | 
						|
mergeIndex :: [Git.Ref] -> Annex ()
 | 
						|
mergeIndex branches = do
 | 
						|
	h <- catFileHandle
 | 
						|
	inRepo $ \g -> Git.UnionMerge.mergeIndex h g branches
 | 
						|
 | 
						|
{- 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
 | 
						|
	f <- fromRepo gitAnnexIndex
 | 
						|
	g <- gitRepo
 | 
						|
	let g' = g { gitEnv = Just [("GIT_INDEX_FILE", f)] }
 | 
						|
 | 
						|
	Annex.changeState $ \s -> s { Annex.repo = g' }
 | 
						|
	checkIndexOnce $ unlessM (liftIO $ doesFileExist f) $ do
 | 
						|
		unless bootstrapping create
 | 
						|
		liftIO $ createDirectoryIfMissing True $ takeDirectory f
 | 
						|
		unless bootstrapping $ inRepo genIndex
 | 
						|
	r <- a
 | 
						|
	Annex.changeState $ \s -> s { Annex.repo = (Annex.repo s) { gitEnv = gitEnv g} }
 | 
						|
 | 
						|
	return r
 | 
						|
 | 
						|
{- Runs an action using the branch's index file, first making sure that
 | 
						|
 - the branch and index are up-to-date. -}
 | 
						|
withIndexUpdate :: Annex a -> Annex a
 | 
						|
withIndexUpdate a = update >> withIndex a
 | 
						|
 | 
						|
{- Updates the branch's index to reflect the current contents of the branch.
 | 
						|
 - Any changes staged in the index will be preserved.
 | 
						|
 -
 | 
						|
 - Compares the ref stored in the lock file with the current
 | 
						|
 - ref of the branch to see if an update is needed.
 | 
						|
 -}
 | 
						|
updateIndex :: Git.Ref -> Annex ()
 | 
						|
updateIndex branchref = do
 | 
						|
	lock <- fromRepo gitAnnexIndexLock
 | 
						|
	lockref <- Git.Ref . firstLine <$>
 | 
						|
		liftIO (catchDefaultIO (readFileStrict lock) "")
 | 
						|
	when (lockref /= branchref) $ do
 | 
						|
		withIndex $ mergeIndex [fullname]
 | 
						|
		setIndexSha branchref
 | 
						|
 | 
						|
{- Record that the branch's index has been updated to correspond to a
 | 
						|
 - given ref of the branch. -}
 | 
						|
setIndexSha :: Git.Ref -> Annex ()
 | 
						|
setIndexSha ref = do
 | 
						|
        lock <- fromRepo gitAnnexIndexLock
 | 
						|
	liftIO $ writeFile lock $ show ref ++ "\n"
 | 
						|
	setAnnexPerm lock
 | 
						|
 | 
						|
{- Checks if there are uncommitted changes in the branch's index or journal. -}
 | 
						|
unCommitted :: Annex Bool
 | 
						|
unCommitted = do
 | 
						|
	d <- liftIO . doesFileExist =<< fromRepo gitAnnexIndexDirty
 | 
						|
	if d
 | 
						|
		then return d
 | 
						|
		else journalDirty
 | 
						|
 | 
						|
setUnCommitted :: Annex ()
 | 
						|
setUnCommitted = do
 | 
						|
	file <- fromRepo gitAnnexIndexDirty
 | 
						|
	liftIO $ writeFile file "1"
 | 
						|
 | 
						|
setCommitted :: Annex ()
 | 
						|
setCommitted = void $ do
 | 
						|
	file <- fromRepo gitAnnexIndexDirty
 | 
						|
	liftIO $ tryIO $ removeFile file
 | 
						|
 | 
						|
{- Stages the journal into the index. -}
 | 
						|
stageJournal :: Annex ()
 | 
						|
stageJournal = do
 | 
						|
	showStoringStateAction
 | 
						|
	fs <- getJournalFiles
 | 
						|
	withIndex $ do
 | 
						|
		g <- gitRepo
 | 
						|
		liftIO $ do
 | 
						|
			h <- hashObjectStart g
 | 
						|
			Git.UpdateIndex.streamUpdateIndex g
 | 
						|
				[genstream (gitAnnexJournalDir g) h fs]
 | 
						|
			hashObjectStop h
 | 
						|
	where
 | 
						|
		genstream dir h fs streamer = forM_ fs $ \file -> do
 | 
						|
			let path = dir </> file
 | 
						|
			sha <- hashFile h path
 | 
						|
			_ <- streamer $ Git.UpdateIndex.updateIndexLine
 | 
						|
				sha FileBlob (asTopFilePath $ fileJournal file)
 | 
						|
			removeFile path
 |