sync: Automatically resolve merge conflict between and annexed file and a regular git file.

This is a new feature, it was not handled before, since it's a bit of an
edge case. However, it can be handled exactly the same as a file/dir
conflict, just leave the non-annexed item alone.

While implementing this, the core resolveMerge' function got a lot simpler
and clearer. Note especially that where before there was an asymetric call to
stagefromdirectmergedir, now graftin is called symmetrically in both cases.

And, in order to add that `graftin us`, the current branch needed to be
known (if there is no current branch, there cannot be a merge conflict).
This led to some cleanups of how autoMergeFrom behaved when there is no
current branch.

This commit was sponsored by Philippe Gauthier.
This commit is contained in:
Joey Hess 2014-03-04 17:45:11 -04:00
parent 1fcc5bef66
commit 14d1e878ab
5 changed files with 81 additions and 77 deletions

View file

@ -16,8 +16,8 @@ import qualified Git.Command
import qualified Git.LsFiles as LsFiles import qualified Git.LsFiles as LsFiles
import qualified Git.UpdateIndex as UpdateIndex import qualified Git.UpdateIndex as UpdateIndex
import qualified Git.Merge import qualified Git.Merge
import qualified Git.Branch
import qualified Git.Ref import qualified Git.Ref
import qualified Git.Sha
import qualified Git import qualified Git
import Git.Types (BlobType(..)) import Git.Types (BlobType(..))
import Config import Config
@ -27,37 +27,36 @@ import Annex.VariantFile
import qualified Data.Set as S import qualified Data.Set as S
{- Merges from a branch into the current branch, with automatic merge {- Merges from a branch into the current branch
- conflict resolution. -} - (which may not exist yet),
autoMergeFrom :: Git.Ref -> Annex Bool - with automatic merge conflict resolution. -}
autoMergeFrom branch = do autoMergeFrom :: Git.Ref -> (Maybe Git.Ref) -> Annex Bool
autoMergeFrom branch currbranch = do
showOutput showOutput
ifM isDirect case currbranch of
( maybe go godirect =<< inRepo Git.Branch.current Nothing -> go Nothing
, go Just b -> go =<< inRepo (Git.Ref.sha b)
)
where where
go = inRepo (Git.Merge.mergeNonInteractive branch) <||> resolveMerge branch go old = ifM isDirect
godirect currbranch = do ( do
old <- inRepo $ Git.Ref.sha currbranch d <- fromRepo gitAnnexMergeDir
d <- fromRepo gitAnnexMergeDir r <- inRepo (mergeDirect d branch)
r <- inRepo (mergeDirect d branch) <||> resolveMerge branch <||> resolveMerge old branch
new <- inRepo $ Git.Ref.sha currbranch mergeDirectCleanup d (fromMaybe Git.Sha.emptyTree old) Git.Ref.headRef
case (old, new) of return r
(Just oldsha, Just newsha) -> , inRepo (Git.Merge.mergeNonInteractive branch)
mergeDirectCleanup d oldsha newsha <||> resolveMerge old branch
_ -> noop )
return r
{- 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
- multiple repositories may be doing this concurrently. - multiple repositories may be doing this concurrently.
- -
- Only annexed files are resolved; other files are left for the user to - Only merge conflicts where at least one side is an annexed file
- handle. - is resolved.
- -
- This uses the Keys pointed to by the files to construct new - This uses the Keys pointed to by the files to construct new
- filenames. So when both sides modified file foo, - filenames. So when both sides modified annexed file foo,
- it will be deleted, and replaced with files foo.variant-A and - it will be deleted, and replaced with files foo.variant-A and
- foo.variant-B. - foo.variant-B.
- -
@ -75,11 +74,11 @@ autoMergeFrom branch = do
- staged to the index, and written to the gitAnnexMergeDir, and later - staged to the index, and written to the gitAnnexMergeDir, and later
- mergeDirectCleanup handles updating the work tree. - mergeDirectCleanup handles updating the work tree.
-} -}
resolveMerge :: Git.Ref -> Annex Bool resolveMerge :: Maybe Git.Ref -> Git.Ref -> Annex Bool
resolveMerge branch = do resolveMerge us them = do
top <- fromRepo Git.repoPath top <- fromRepo Git.repoPath
(fs, cleanup) <- inRepo (LsFiles.unmerged [top]) (fs, cleanup) <- inRepo (LsFiles.unmerged [top])
mergedfs <- catMaybes <$> mapM (resolveMerge' branch) fs mergedfs <- catMaybes <$> mapM (resolveMerge' us them) fs
let merged = not (null mergedfs) let merged = not (null mergedfs)
void $ liftIO cleanup void $ liftIO cleanup
@ -93,48 +92,50 @@ resolveMerge branch = do
unlessM isDirect $ unlessM isDirect $
cleanConflictCruft mergedfs top cleanConflictCruft mergedfs top
Annex.Queue.flush Annex.Queue.flush
whenM isDirect $
void preCommitDirect
void $ inRepo $ Git.Command.runBool void $ inRepo $ Git.Command.runBool
[ Param "commit" [ Param "commit"
, Param "--no-verify"
, Param "-m" , Param "-m"
, Param "git-annex automatic merge conflict fix" , Param "git-annex automatic merge conflict fix"
] ]
showLongNote "Merge conflict was automatically resolved; you may want to examine the result." showLongNote "Merge conflict was automatically resolved; you may want to examine the result."
return merged return merged
resolveMerge' :: Git.Ref -> LsFiles.Unmerged -> Annex (Maybe FilePath) resolveMerge' :: Maybe Git.Ref -> Git.Ref -> LsFiles.Unmerged -> Annex (Maybe FilePath)
resolveMerge' branch u resolveMerge' Nothing _ _ = return Nothing
| mergeable LsFiles.valUs && mergeable LsFiles.valThem = do resolveMerge' (Just us) them u = do
kus <- getKey LsFiles.valUs kus <- getkey LsFiles.valUs LsFiles.valUs
kthem <- getKey LsFiles.valThem kthem <- getkey LsFiles.valThem LsFiles.valThem
case (kus, kthem) of case (kus, kthem) of
-- Both sides of conflict are annexed files -- Both sides of conflict are annexed files
(Just keyUs, Just keyThem) -> do (Just keyUs, Just keyThem) -> resolveby $
unstageoldfile if keyUs == keyThem
if keyUs == keyThem then makelink keyUs
then makelink keyUs else do
else do makelink keyUs
makelink keyUs makelink keyThem
makelink keyThem -- Our side is annexed file, other side is not.
return $ Just file (Just keyUs, Nothing) -> resolveby $ do
-- Our side is annexed, other side is not. graftin them file
(Just keyUs, Nothing) -> do makelink keyUs
unstageoldfile -- Our side is not annexed file, other side is.
whenM isDirect $ (Nothing, Just keyThem) -> resolveby $ do
stagefromdirectmergedir file graftin us file
makelink keyUs makelink keyThem
return $ Just file -- Neither side is annexed file; cannot resolve.
-- Our side is not annexed, other side is. (Nothing, Nothing) -> return Nothing
(Nothing, Just keyThem) -> do
unstageoldfile
makelink keyThem
return $ Just file
-- Neither side is annexed; cannot resolve.
(Nothing, Nothing) -> return Nothing
| otherwise = return Nothing
where where
file = LsFiles.unmergedFile u file = LsFiles.unmergedFile u
mergeable select = select (LsFiles.unmergedBlobType u)
`elem` [Just SymlinkBlob, Nothing] getkey select select'
| select (LsFiles.unmergedBlobType u) == Just SymlinkBlob =
case select' (LsFiles.unmergedSha u) of
Nothing -> return Nothing
Just sha -> catKey sha symLinkMode
| otherwise = return Nothing
makelink key = do makelink key = do
let dest = variantFile file key let dest = variantFile file key
l <- inRepo $ gitAnnexLink dest key l <- inRepo $ gitAnnexLink dest key
@ -145,17 +146,16 @@ resolveMerge' branch u
, replaceFile dest $ makeAnnexLink l , replaceFile dest $ makeAnnexLink l
) )
stageSymlink dest =<< hashSymlink l stageSymlink dest =<< hashSymlink l
getKey select = case select (LsFiles.unmergedSha u) of
Nothing -> return Nothing
Just sha -> catKey sha symLinkMode
-- removing the conflicted file from cache clears the conflict {- stage a graft of a directory or file from a branch -}
unstageoldfile = Annex.Queue.addCommand "rm" [Params "--quiet -f --cached --"] [file] graftin b item = Annex.Queue.addUpdateIndex
=<< fromRepo (UpdateIndex.lsSubTree b item)
{- stage an item from the direct mode merge directory, which may resolveby a = do
- be a directory with arbitrary contents -} {- Remove conflicted file from index so merge can be resolved. -}
stagefromdirectmergedir item = Annex.Queue.addUpdateIndex Annex.Queue.addCommand "rm" [Params "--quiet -f --cached --"] [file]
=<< fromRepo (UpdateIndex.lsSubTree branch item) void a
return (Just file)
{- git-merge moves conflicting files away to files {- git-merge moves conflicting files away to files
- named something like f~HEAD or f~branch, but the - named something like f~HEAD or f~branch, but the

View file

@ -83,7 +83,7 @@ onChange file
[ "merging", Git.fromRef changedbranch [ "merging", Git.fromRef changedbranch
, "into", Git.fromRef current , "into", Git.fromRef current
] ]
void $ liftAnnex $ autoMergeFrom changedbranch void $ liftAnnex $ autoMergeFrom changedbranch (Just current)
mergecurrent _ = noop mergecurrent _ = noop
handleDesynced = case fromTaggedBranch changedbranch of handleDesynced = case fromTaggedBranch changedbranch of

View file

@ -169,7 +169,7 @@ mergeLocal (Just branch) = go =<< needmerge
go False = stop go False = stop
go True = do go True = do
showStart "merge" $ Git.Ref.describe syncbranch showStart "merge" $ Git.Ref.describe syncbranch
next $ next $ autoMergeFrom syncbranch next $ next $ autoMergeFrom syncbranch (Just branch)
pushLocal :: Maybe Git.Ref -> CommandStart pushLocal :: Maybe Git.Ref -> CommandStart
pushLocal Nothing = stop pushLocal Nothing = stop
@ -213,10 +213,11 @@ mergeRemote :: Remote -> Maybe Git.Ref -> CommandCleanup
mergeRemote remote b = case b of mergeRemote remote b = case b of
Nothing -> do Nothing -> do
branch <- inRepo Git.Branch.currentUnsafe branch <- inRepo Git.Branch.currentUnsafe
and <$> mapM merge (branchlist branch) and <$> mapM (merge Nothing) (branchlist branch)
Just _ -> and <$> (mapM merge =<< tomerge (branchlist b)) Just thisbranch ->
and <$> (mapM (merge (Just thisbranch)) =<< tomerge (branchlist b))
where where
merge = autoMergeFrom . remoteBranch remote merge thisbranch = flip autoMergeFrom thisbranch . remoteBranch remote
tomerge = filterM (changed remote) tomerge = filterM (changed remote)
branchlist Nothing = [] branchlist Nothing = []
branchlist (Just branch) = [branch, syncBranch branch] branchlist (Just branch) = [branch, syncBranch branch]

View file

@ -925,8 +925,8 @@ test_nonannexed_conflict_resolution env = do
when switchdirect $ when switchdirect $
git_annex env "direct" [] @? "failed switching to direct mode" git_annex env "direct" [] @? "failed switching to direct mode"
git_annex env "sync" [] @? "sync failed" git_annex env "sync" [] @? "sync failed"
checkmerge "r1" r1 checkmerge ("r1" ++ show switchdirect) r1
checkmerge "r2" r2 checkmerge ("r2" ++ show switchdirect) r2
conflictor = "conflictor" conflictor = "conflictor"
nonannexed_content = "nonannexed" nonannexed_content = "nonannexed"
variantprefix = conflictor ++ ".variant" variantprefix = conflictor ++ ".variant"
@ -936,8 +936,9 @@ test_nonannexed_conflict_resolution env = do
not (null v) not (null v)
@? (what ++ " conflictor variant file missing in: " ++ show l ) @? (what ++ " conflictor variant file missing in: " ++ show l )
conflictor `elem` l @? (what ++ " conflictor file missing in: " ++ show l) conflictor `elem` l @? (what ++ " conflictor file missing in: " ++ show l)
s <- readFile (d </> conflictor) s <- catchMaybeIO (readFile (d </> conflictor))
s == nonannexed_content @? (what ++ " wrong content for nonannexed file: " ++ s) s == Just nonannexed_content
@? (what ++ " wrong content for nonannexed file: " ++ show s)
{- Check merge conflict resolution when there is a local file, {- Check merge conflict resolution when there is a local file,
- that is not staged or committed, that conflicts with what's being added - that is not staged or committed, that conflicts with what's being added

2
debian/changelog vendored
View file

@ -22,6 +22,8 @@ git-annex (5.20140228) UNRELEASED; urgency=medium
* metadata: To only set a field when it's not already got a value, use * metadata: To only set a field when it's not already got a value, use
-s field?=value -s field?=value
* Run .git/hooks/pre-commit-annex whenever a commit is made. * Run .git/hooks/pre-commit-annex whenever a commit is made.
* sync: Automatically resolve merge conflict between and annexed file
and a regular git file.
-- Joey Hess <joeyh@debian.org> Fri, 28 Feb 2014 14:52:15 -0400 -- Joey Hess <joeyh@debian.org> Fri, 28 Feb 2014 14:52:15 -0400