direct mode merging works!

Automatic merge resoltion code needs to be fixed to preserve objects from
direct mode files.
This commit is contained in:
Joey Hess 2012-12-18 15:04:44 -04:00
parent d62a58b9c8
commit 53dbcce645
6 changed files with 135 additions and 62 deletions

View file

@ -9,7 +9,6 @@ module Annex.Content.Direct (
associatedFiles, associatedFiles,
removeAssociatedFile, removeAssociatedFile,
addAssociatedFile, addAssociatedFile,
updateAssociatedFiles,
goodContent, goodContent,
updateCache, updateCache,
recordedCache, recordedCache,
@ -23,11 +22,7 @@ module Annex.Content.Direct (
import Common.Annex import Common.Annex
import qualified Git import qualified Git
import qualified Git.DiffTree as DiffTree
import Git.Sha
import Annex.CatFile
import Utility.TempFile import Utility.TempFile
import Utility.FileMode
import Logs.Location import Logs.Location
import System.Posix.Types import System.Posix.Types
@ -70,23 +65,6 @@ addAssociatedFile key file = changeAssociatedFiles key $ \files ->
then files then files
else file:files else file:files
{- Uses git diff-tree to find files changed between two tree Shas, and
- updates the associated file mappings, efficiently. -}
updateAssociatedFiles :: Git.Sha -> Git.Sha -> Annex ()
updateAssociatedFiles oldsha newsha = do
(items, cleanup) <- inRepo $ DiffTree.diffTree oldsha newsha
forM_ items update
void $ liftIO $ cleanup
where
update item = do
go DiffTree.dstsha DiffTree.dstmode addAssociatedFile
go DiffTree.srcsha DiffTree.srcmode removeAssociatedFile
where
go getsha getmode a =
when (getsha item /= nullSha && isSymLink (getmode item)) $ do
key <- catKey (getsha item)
maybe noop (\k -> void $ a k $ DiffTree.file item) key
{- Checks if a file in the tree, associated with a key, has not been modified. {- Checks if a file in the tree, associated with a key, has not been modified.
- -
- To avoid needing to fsck the file's content, which can involve an - To avoid needing to fsck the file's content, which can involve an

View file

@ -12,9 +12,13 @@ import qualified Git
import qualified Git.LsFiles import qualified Git.LsFiles
import qualified Git.UpdateIndex import qualified Git.UpdateIndex
import qualified Git.HashObject import qualified Git.HashObject
import qualified Annex.Queue import qualified Git.Merge
import qualified Git.DiffTree as DiffTree
import Git.Sha
import Git.Types import Git.Types
import Annex.CatFile import Annex.CatFile
import Utility.FileMode
import qualified Annex.Queue
import Logs.Location import Logs.Location
import Backend import Backend
import Types.KeySource import Types.KeySource
@ -103,3 +107,93 @@ addDirect file cache = do
showEndFail showEndFail
return False return False
) )
{- In direct mode, git merge would usually refuse to do anything, since it
- sees present direct mode files as type changed files. To avoid this,
- merge is run with the work tree set to a temp directory.
-
- This should only be used once any changes to the real working tree have
- already been committed, because it overwrites files in the working tree.
-}
mergeDirect :: FilePath -> Git.Ref -> Git.Repo -> IO Bool
mergeDirect d branch g = do
createDirectoryIfMissing True d
let g' = g { location = Local { gitdir = Git.localGitDir g, worktree = Just d } }
Git.Merge.mergeNonInteractive branch g'
{- Cleans up after a direct mode merge. The merge must have been committed,
- and the commit sha passed in, along with the old sha of the tree
- before the merge. Uses git diff-tree to find files that changed between
- the two shas, and applies those changes to the work tree.
-}
mergeDirectCleanup :: FilePath -> Git.Ref -> Git.Ref -> Annex ()
mergeDirectCleanup d oldsha newsha = do
(items, cleanup) <- inRepo $ DiffTree.diffTreeRecursive oldsha newsha
forM_ items updated
void $ liftIO $ cleanup
liftIO $ removeDirectoryRecursive d
where
updated item = do
go DiffTree.srcsha DiffTree.srcmode moveout moveout_raw
go DiffTree.dstsha DiffTree.dstmode movein movein_raw
where
go getsha getmode a araw
| getsha item == nullSha = noop
| isSymLink (getmode item) =
maybe (araw f) (\k -> void $ a k f)
=<< catKey (getsha item)
| otherwise = araw f
f = DiffTree.file item
{- Any content that was present in direct mode and whose file is to
- be modified or deleted by the merge is first moved to
- .git/annex/objects, unless there are other associated files for
- the content. No content is ever lost due to a direct mode merge. -}
moveout k f = do
locs <- removeAssociatedFile k f
when (null locs) $ do
r <- liftIO $ catchMaybeIO $ getSymbolicLinkStatus f
case r of
Just s
| not (isSymbolicLink s) ->
moveAnnex k f
_ -> noop
moveout_raw f
{- Files deleted by the merge are removed from the work tree.
- Empty work tree directories are removed, per git behavior. -}
moveout_raw f = liftIO $ do
nukeFile f
void $ catchMaybeIO $ removeDirectory $ parentDir f
{- Key symlinks are replaced with their content, if it's available. -}
movein k f = do
movein_raw f
maybe noop id =<< toDirect k f
{- Any new, modified, or renamed files were written to the temp
- directory by the merge, and are moved to the real work tree. -}
movein_raw f = liftIO $ do
createDirectoryIfMissing True $ parentDir f
rename (d </> f) f
{- If possible, returns an action that will convert a symlink in the
- working tree into a direct mode file. -}
toDirect :: Key -> FilePath -> Annex (Maybe (Annex ()))
toDirect k f = do
loc <- inRepo $ gitAnnexLocation k
createContentDir loc -- thaws directory too
locs <- filter (/= f) <$> addAssociatedFile k f
case locs of
[] -> ifM (liftIO $ doesFileExist loc)
( return $ Just $ do
{- Move content from annex to direct file. -}
updateCache k loc
thawContent loc
liftIO $ replaceFile f $ moveFile loc
, return Nothing
)
(loc':_) -> return $ Just $ do
{- Another direct file has the content, so
- hard link to it. -}
liftIO $ replaceFile f $ createLink loc'

View file

@ -13,8 +13,7 @@ import qualified Git
import qualified Git.Command import qualified Git.Command
import qualified Git.LsFiles import qualified Git.LsFiles
import Config import Config
import Annex.Content import Annex.Direct
import Annex.Content.Direct
def :: [Command] def :: [Command]
def = [command "direct" paramNothing seek "switch repository to direct mode"] def = [command "direct" paramNothing seek "switch repository to direct mode"]
@ -41,25 +40,13 @@ perform = do
void $ liftIO clean void $ liftIO clean
next cleanup next cleanup
where where
{- Walk tree from top and move all present objects to the
- files that link to them, while updating direct mode mappings. -}
go = whenAnnexed $ \f (k, _) -> do go = whenAnnexed $ \f (k, _) -> do
loc <- inRepo $ gitAnnexLocation k r <- toDirect k f
createContentDir loc -- thaws directory too case r of
locs <- filter (/= f) <$> addAssociatedFile k f Nothing -> noop
case locs of Just a -> do
[] -> whenM (liftIO $ doesFileExist loc) $ do
{- Move content from annex to direct file. -}
showStart "direct" f showStart "direct" f
updateCache k loc a
thawContent loc
liftIO $ replaceFile f $ moveFile loc
showEndOk
(loc':_) -> do
{- Another direct file has the content, so
- hard link to it. -}
showStart "direct" f
liftIO $ replaceFile f $ createLink loc'
showEndOk showEndOk
return Nothing return Nothing

View file

@ -15,7 +15,6 @@ import qualified Annex
import qualified Annex.Branch import qualified Annex.Branch
import qualified Annex.Queue import qualified Annex.Queue
import Annex.Content import Annex.Content
import Annex.Content.Direct
import Annex.Direct import Annex.Direct
import Annex.CatFile import Annex.CatFile
import qualified Git.Command import qualified Git.Command
@ -179,31 +178,30 @@ mergeAnnex = do
void $ Annex.Branch.forceUpdate void $ Annex.Branch.forceUpdate
stop stop
{- Merges from a branch into the current branch. {- Merges from a branch into the current branch. -}
-
- In direct mode, updates associated files mappings for the files that
- were changed by the merge. -}
mergeFrom :: Git.Ref -> Annex Bool mergeFrom :: Git.Ref -> Annex Bool
mergeFrom branch = ifM isDirect mergeFrom branch = do
showOutput
ifM isDirect
( maybe go godirect =<< inRepo Git.Branch.current ( maybe go godirect =<< inRepo Git.Branch.current
, go , go
) )
where where
go = do go = runmerge $ inRepo $ Git.Merge.mergeNonInteractive branch
showOutput
ok <- inRepo $ Git.Merge.mergeNonInteractive branch
if ok
then return ok
else resolveMerge
godirect currbranch = do godirect currbranch = do
old <- inRepo $ Git.Ref.sha currbranch old <- inRepo $ Git.Ref.sha currbranch
r <- go d <- fromRepo gitAnnexMergeDir
r <- runmerge $ inRepo $ mergeDirect d branch
new <- inRepo $ Git.Ref.sha currbranch new <- inRepo $ Git.Ref.sha currbranch
case (old, new) of case (old, new) of
(Just oldsha, Just newsha) -> do (Just oldsha, Just newsha) ->
updateAssociatedFiles oldsha newsha mergeDirectCleanup d oldsha newsha
_ -> noop _ -> noop
return r return r
runmerge a = ifM (a)
( return True
, resolveMerge
)
{- Resolves a conflicted merge. It's important that any conflicts be {- Resolves a conflicted merge. It's important that any conflicts be
- resolved in a way that itself avoids later merge conflicts, since - resolved in a way that itself avoids later merge conflicts, since

View file

@ -8,6 +8,7 @@
module Git.DiffTree ( module Git.DiffTree (
DiffTreeItem(..), DiffTreeItem(..),
diffTree, diffTree,
diffTreeRecursive,
parseDiffTree parseDiffTree
) where ) where
@ -31,9 +32,19 @@ data DiffTreeItem = DiffTreeItem
{- Diffs two tree Refs. -} {- Diffs two tree Refs. -}
diffTree :: Ref -> Ref -> Repo -> IO ([DiffTreeItem], IO Bool) diffTree :: Ref -> Ref -> Repo -> IO ([DiffTreeItem], IO Bool)
diffTree src dst repo = do diffTree = diffTree' []
(diff, cleanup) <- pipeNullSplit [Params "diff-tree -z --raw --no-renames -l0", Param (show src), Param (show dst)] repo
{- Diffs two tree Refs, recursing into sub-trees -}
diffTreeRecursive :: Ref -> Ref -> Repo -> IO ([DiffTreeItem], IO Bool)
diffTreeRecursive = diffTree' [Param "-r"]
diffTree' :: [CommandParam] -> Ref -> Ref -> Repo -> IO ([DiffTreeItem], IO Bool)
diffTree' params src dst repo = do
(diff, cleanup) <- pipeNullSplit ps repo
return (parseDiffTree diff, cleanup) return (parseDiffTree diff, cleanup)
where
ps = Params "diff-tree -z --raw --no-renames -l0" : params ++
[Param (show src), Param (show dst)]
{- Parses diff-tree output. -} {- Parses diff-tree output. -}
parseDiffTree :: [String] -> [DiffTreeItem] parseDiffTree :: [String] -> [DiffTreeItem]

View file

@ -25,6 +25,7 @@ module Locations (
gitAnnexFsckState, gitAnnexFsckState,
gitAnnexTransferDir, gitAnnexTransferDir,
gitAnnexCredsDir, gitAnnexCredsDir,
gitAnnexMergeDir,
gitAnnexJournalDir, gitAnnexJournalDir,
gitAnnexJournalLock, gitAnnexJournalLock,
gitAnnexIndex, gitAnnexIndex,
@ -161,6 +162,10 @@ gitAnnexFsckState r = gitAnnexDir r </> "fsckstate"
gitAnnexCredsDir :: Git.Repo -> FilePath gitAnnexCredsDir :: Git.Repo -> FilePath
gitAnnexCredsDir r = addTrailingPathSeparator $ gitAnnexDir r </> "creds" gitAnnexCredsDir r = addTrailingPathSeparator $ gitAnnexDir r </> "creds"
{- .git/annex/merge/ is used for direct mode merges. -}
gitAnnexMergeDir :: Git.Repo -> FilePath
gitAnnexMergeDir r = addTrailingPathSeparator $ gitAnnexDir r </> "merge"
{- .git/annex/transfer/ is used to record keys currently {- .git/annex/transfer/ is used to record keys currently
- being transferred, and other transfer bookkeeping info. -} - being transferred, and other transfer bookkeeping info. -}
gitAnnexTransferDir :: Git.Repo -> FilePath gitAnnexTransferDir :: Git.Repo -> FilePath