factor out Annex.AutoMerge from Command.Sync
This commit is contained in:
parent
33aae51175
commit
99295f2c1d
3 changed files with 184 additions and 164 deletions
179
Annex/AutoMerge.hs
Normal file
179
Annex/AutoMerge.hs
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
{- git-annex automatic merge conflict resolution
|
||||||
|
-
|
||||||
|
- Copyright 2012-2014 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Annex.AutoMerge (autoMergeFrom) where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import qualified Annex.Queue
|
||||||
|
import Annex.Direct
|
||||||
|
import Annex.CatFile
|
||||||
|
import Annex.Link
|
||||||
|
import qualified Git.Command
|
||||||
|
import qualified Git.LsFiles as LsFiles
|
||||||
|
import qualified Git.UpdateIndex as UpdateIndex
|
||||||
|
import qualified Git.Merge
|
||||||
|
import qualified Git.Branch
|
||||||
|
import qualified Git.Ref
|
||||||
|
import qualified Git
|
||||||
|
import Git.Types (BlobType(..))
|
||||||
|
import Config
|
||||||
|
import Annex.ReplaceFile
|
||||||
|
import Git.FileMode
|
||||||
|
import Annex.VariantFile
|
||||||
|
|
||||||
|
import qualified Data.Set as S
|
||||||
|
|
||||||
|
{- Merges from a branch into the current branch, with automatic merge
|
||||||
|
- conflict resolution. -}
|
||||||
|
autoMergeFrom :: Git.Ref -> Annex Bool
|
||||||
|
autoMergeFrom branch = do
|
||||||
|
showOutput
|
||||||
|
ifM isDirect
|
||||||
|
( maybe go godirect =<< inRepo Git.Branch.current
|
||||||
|
, go
|
||||||
|
)
|
||||||
|
where
|
||||||
|
go = inRepo (Git.Merge.mergeNonInteractive branch) <||> resolveMerge branch
|
||||||
|
godirect currbranch = do
|
||||||
|
old <- inRepo $ Git.Ref.sha currbranch
|
||||||
|
d <- fromRepo gitAnnexMergeDir
|
||||||
|
r <- inRepo (mergeDirect d branch) <||> resolveMerge branch
|
||||||
|
new <- inRepo $ Git.Ref.sha currbranch
|
||||||
|
case (old, new) of
|
||||||
|
(Just oldsha, Just newsha) ->
|
||||||
|
mergeDirectCleanup d oldsha newsha
|
||||||
|
_ -> noop
|
||||||
|
return r
|
||||||
|
|
||||||
|
{- Resolves a conflicted merge. It's important that any conflicts be
|
||||||
|
- resolved in a way that itself avoids later merge conflicts, since
|
||||||
|
- multiple repositories may be doing this concurrently.
|
||||||
|
-
|
||||||
|
- Only annexed files are resolved; other files are left for the user to
|
||||||
|
- handle.
|
||||||
|
-
|
||||||
|
- This uses the Keys pointed to by the files to construct new
|
||||||
|
- filenames. So when both sides modified file foo,
|
||||||
|
- it will be deleted, and replaced with files foo.variant-A and
|
||||||
|
- foo.variant-B.
|
||||||
|
-
|
||||||
|
- On the other hand, when one side deleted foo, and the other modified it,
|
||||||
|
- it will be deleted, and the modified version stored as file
|
||||||
|
- foo.variant-A (or B).
|
||||||
|
-
|
||||||
|
- It's also possible that one side has foo as an annexed file, and
|
||||||
|
- the other as a directory or non-annexed file. The annexed file
|
||||||
|
- is renamed to resolve the merge, and the other object is preserved as-is.
|
||||||
|
-
|
||||||
|
- In indirect mode, the merge is resolved in the work tree and files
|
||||||
|
- staged, to clean up from a conflicted merge that was run in the work
|
||||||
|
- tree. In direct mode, the work tree is not touched here; files are
|
||||||
|
- staged to the index, and written to the gitAnnexMergeDir, and later
|
||||||
|
- mergeDirectCleanup handles updating the work tree.
|
||||||
|
-}
|
||||||
|
resolveMerge :: Git.Ref -> Annex Bool
|
||||||
|
resolveMerge branch = do
|
||||||
|
top <- fromRepo Git.repoPath
|
||||||
|
(fs, cleanup) <- inRepo (LsFiles.unmerged [top])
|
||||||
|
mergedfs <- catMaybes <$> mapM (resolveMerge' branch) fs
|
||||||
|
let merged = not (null mergedfs)
|
||||||
|
void $ liftIO cleanup
|
||||||
|
|
||||||
|
unlessM isDirect $ do
|
||||||
|
(deleted, cleanup2) <- inRepo (LsFiles.deleted [top])
|
||||||
|
unless (null deleted) $
|
||||||
|
Annex.Queue.addCommand "rm" [Params "--quiet -f --"] deleted
|
||||||
|
void $ liftIO cleanup2
|
||||||
|
|
||||||
|
when merged $ do
|
||||||
|
unlessM isDirect $
|
||||||
|
cleanConflictCruft mergedfs top
|
||||||
|
Annex.Queue.flush
|
||||||
|
void $ inRepo $ Git.Command.runBool
|
||||||
|
[ Param "commit"
|
||||||
|
, Param "-m"
|
||||||
|
, Param "git-annex automatic merge conflict fix"
|
||||||
|
]
|
||||||
|
showLongNote "Merge conflict was automatically resolved; you may want to examine the result."
|
||||||
|
return merged
|
||||||
|
|
||||||
|
resolveMerge' :: Git.Ref -> LsFiles.Unmerged -> Annex (Maybe FilePath)
|
||||||
|
resolveMerge' branch u
|
||||||
|
| mergeable LsFiles.valUs && mergeable LsFiles.valThem = do
|
||||||
|
kus <- getKey LsFiles.valUs
|
||||||
|
kthem <- getKey LsFiles.valThem
|
||||||
|
case (kus, kthem) of
|
||||||
|
-- Both sides of conflict are annexed files
|
||||||
|
(Just keyUs, Just keyThem) -> do
|
||||||
|
unstageoldfile
|
||||||
|
if keyUs == keyThem
|
||||||
|
then makelink keyUs
|
||||||
|
else do
|
||||||
|
makelink keyUs
|
||||||
|
makelink keyThem
|
||||||
|
return $ Just file
|
||||||
|
-- Our side is annexed, other side is not.
|
||||||
|
(Just keyUs, Nothing) -> do
|
||||||
|
unstageoldfile
|
||||||
|
whenM isDirect $
|
||||||
|
stagefromdirectmergedir file
|
||||||
|
makelink keyUs
|
||||||
|
return $ Just file
|
||||||
|
-- Our side is not annexed, other side is.
|
||||||
|
(Nothing, Just keyThem) -> do
|
||||||
|
unstageoldfile
|
||||||
|
makelink keyThem
|
||||||
|
return $ Just file
|
||||||
|
-- Neither side is annexed; cannot resolve.
|
||||||
|
(Nothing, Nothing) -> return Nothing
|
||||||
|
| otherwise = return Nothing
|
||||||
|
where
|
||||||
|
file = LsFiles.unmergedFile u
|
||||||
|
mergeable select = select (LsFiles.unmergedBlobType u)
|
||||||
|
`elem` [Just SymlinkBlob, Nothing]
|
||||||
|
makelink key = do
|
||||||
|
let dest = variantFile file key
|
||||||
|
l <- inRepo $ gitAnnexLink dest key
|
||||||
|
ifM isDirect
|
||||||
|
( do
|
||||||
|
d <- fromRepo gitAnnexMergeDir
|
||||||
|
replaceFile (d </> dest) $ makeAnnexLink l
|
||||||
|
, replaceFile dest $ makeAnnexLink 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
|
||||||
|
unstageoldfile = Annex.Queue.addCommand "rm" [Params "--quiet -f --cached --"] [file]
|
||||||
|
|
||||||
|
{- stage an item from the direct mode merge directory, which may
|
||||||
|
- be a directory with arbitrary contents -}
|
||||||
|
stagefromdirectmergedir item = Annex.Queue.addUpdateIndex
|
||||||
|
=<< fromRepo (UpdateIndex.lsSubTree branch item)
|
||||||
|
|
||||||
|
{- git-merge moves conflicting files away to files
|
||||||
|
- named something like f~HEAD or f~branch, but the
|
||||||
|
- exact name chosen can vary. Once the conflict is resolved,
|
||||||
|
- this cruft can be deleted. To avoid deleting legitimate
|
||||||
|
- files that look like this, only delete files that are
|
||||||
|
- A) not staged in git and B) look like git-annex symlinks.
|
||||||
|
-}
|
||||||
|
cleanConflictCruft :: [FilePath] -> FilePath -> Annex ()
|
||||||
|
cleanConflictCruft resolvedfs top = do
|
||||||
|
(fs, cleanup) <- inRepo $ LsFiles.notInRepo False [top]
|
||||||
|
mapM_ clean fs
|
||||||
|
void $ liftIO cleanup
|
||||||
|
where
|
||||||
|
clean f
|
||||||
|
| matchesresolved f = whenM (isJust <$> isAnnexLink f) $
|
||||||
|
liftIO $ nukeFile f
|
||||||
|
| otherwise = noop
|
||||||
|
s = S.fromList resolvedfs
|
||||||
|
matchesresolved f = S.member (base f) s
|
||||||
|
base f = reverse $ drop 1 $ dropWhile (/= '~') $ reverse f
|
|
@ -17,7 +17,7 @@ import Utility.DirWatcher.Types
|
||||||
import qualified Annex.Branch
|
import qualified Annex.Branch
|
||||||
import qualified Git
|
import qualified Git
|
||||||
import qualified Git.Branch
|
import qualified Git.Branch
|
||||||
import qualified Command.Sync
|
import Annex.AutoMerge
|
||||||
import Annex.TaggedPush
|
import Annex.TaggedPush
|
||||||
import Remote (remoteFromUUID)
|
import Remote (remoteFromUUID)
|
||||||
|
|
||||||
|
@ -83,7 +83,7 @@ onChange file
|
||||||
[ "merging", Git.fromRef changedbranch
|
[ "merging", Git.fromRef changedbranch
|
||||||
, "into", Git.fromRef current
|
, "into", Git.fromRef current
|
||||||
]
|
]
|
||||||
void $ liftAnnex $ Command.Sync.mergeFrom changedbranch
|
void $ liftAnnex $ autoMergeFrom changedbranch
|
||||||
mergecurrent _ = noop
|
mergecurrent _ = noop
|
||||||
|
|
||||||
handleDesynced = case fromTaggedBranch changedbranch of
|
handleDesynced = case fromTaggedBranch changedbranch of
|
||||||
|
|
165
Command/Sync.hs
165
Command/Sync.hs
|
@ -12,26 +12,18 @@ import Common.Annex
|
||||||
import Command
|
import Command
|
||||||
import qualified Annex
|
import qualified Annex
|
||||||
import qualified Annex.Branch
|
import qualified Annex.Branch
|
||||||
import qualified Annex.Queue
|
|
||||||
import qualified Remote
|
import qualified Remote
|
||||||
import qualified Types.Remote as Remote
|
import qualified Types.Remote as Remote
|
||||||
import Annex.Direct
|
import Annex.Direct
|
||||||
import Annex.CatFile
|
|
||||||
import Annex.Link
|
|
||||||
import Annex.Hook
|
import Annex.Hook
|
||||||
import qualified Git.Command
|
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.Merge
|
|
||||||
import qualified Git.Branch
|
import qualified Git.Branch
|
||||||
import qualified Git.Ref
|
import qualified Git.Ref
|
||||||
import qualified Git
|
import qualified Git
|
||||||
import Git.Types (BlobType(..))
|
|
||||||
import qualified Types.Remote
|
import qualified Types.Remote
|
||||||
import qualified Remote.Git
|
import qualified Remote.Git
|
||||||
import Config
|
import Config
|
||||||
import Annex.ReplaceFile
|
|
||||||
import Git.FileMode
|
|
||||||
import Annex.Wanted
|
import Annex.Wanted
|
||||||
import Annex.Content
|
import Annex.Content
|
||||||
import Command.Get (getKeyFile')
|
import Command.Get (getKeyFile')
|
||||||
|
@ -39,9 +31,8 @@ import qualified Command.Move
|
||||||
import Logs.Location
|
import Logs.Location
|
||||||
import Annex.Drop
|
import Annex.Drop
|
||||||
import Annex.UUID
|
import Annex.UUID
|
||||||
import Annex.VariantFile
|
import Annex.AutoMerge
|
||||||
|
|
||||||
import qualified Data.Set as S
|
|
||||||
import Control.Concurrent.MVar
|
import Control.Concurrent.MVar
|
||||||
|
|
||||||
def :: [Command]
|
def :: [Command]
|
||||||
|
@ -178,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 $ mergeFrom syncbranch
|
next $ next $ autoMergeFrom syncbranch
|
||||||
|
|
||||||
pushLocal :: Maybe Git.Ref -> CommandStart
|
pushLocal :: Maybe Git.Ref -> CommandStart
|
||||||
pushLocal Nothing = stop
|
pushLocal Nothing = stop
|
||||||
|
@ -225,7 +216,7 @@ mergeRemote remote b = case b of
|
||||||
and <$> mapM merge (branchlist branch)
|
and <$> mapM merge (branchlist branch)
|
||||||
Just _ -> and <$> (mapM merge =<< tomerge (branchlist b))
|
Just _ -> and <$> (mapM merge =<< tomerge (branchlist b))
|
||||||
where
|
where
|
||||||
merge = mergeFrom . remoteBranch remote
|
merge = autoMergeFrom . 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]
|
||||||
|
@ -306,156 +297,6 @@ mergeAnnex = do
|
||||||
void Annex.Branch.forceUpdate
|
void Annex.Branch.forceUpdate
|
||||||
stop
|
stop
|
||||||
|
|
||||||
{- Merges from a branch into the current branch. -}
|
|
||||||
mergeFrom :: Git.Ref -> Annex Bool
|
|
||||||
mergeFrom branch = do
|
|
||||||
showOutput
|
|
||||||
ifM isDirect
|
|
||||||
( maybe go godirect =<< inRepo Git.Branch.current
|
|
||||||
, go
|
|
||||||
)
|
|
||||||
where
|
|
||||||
go = inRepo (Git.Merge.mergeNonInteractive branch) <||> resolveMerge branch
|
|
||||||
godirect currbranch = do
|
|
||||||
old <- inRepo $ Git.Ref.sha currbranch
|
|
||||||
d <- fromRepo gitAnnexMergeDir
|
|
||||||
r <- inRepo (mergeDirect d branch) <||> resolveMerge branch
|
|
||||||
new <- inRepo $ Git.Ref.sha currbranch
|
|
||||||
case (old, new) of
|
|
||||||
(Just oldsha, Just newsha) ->
|
|
||||||
mergeDirectCleanup d oldsha newsha
|
|
||||||
_ -> noop
|
|
||||||
return r
|
|
||||||
|
|
||||||
{- Resolves a conflicted merge. It's important that any conflicts be
|
|
||||||
- resolved in a way that itself avoids later merge conflicts, since
|
|
||||||
- multiple repositories may be doing this concurrently.
|
|
||||||
-
|
|
||||||
- Only annexed files are resolved; other files are left for the user to
|
|
||||||
- handle.
|
|
||||||
-
|
|
||||||
- This uses the Keys pointed to by the files to construct new
|
|
||||||
- filenames. So when both sides modified file foo,
|
|
||||||
- it will be deleted, and replaced with files foo.variant-A and
|
|
||||||
- foo.variant-B.
|
|
||||||
-
|
|
||||||
- On the other hand, when one side deleted foo, and the other modified it,
|
|
||||||
- it will be deleted, and the modified version stored as file
|
|
||||||
- foo.variant-A (or B).
|
|
||||||
-
|
|
||||||
- It's also possible that one side has foo as an annexed file, and
|
|
||||||
- the other as a directory or non-annexed file. The annexed file
|
|
||||||
- is renamed to resolve the merge, and the other object is preserved as-is.
|
|
||||||
-
|
|
||||||
- In indirect mode, the merge is resolved in the work tree and files
|
|
||||||
- staged, to clean up from a conflicted merge that was run in the work
|
|
||||||
- tree. In direct mode, the work tree is not touched here; files are
|
|
||||||
- staged to the index, and written to the gitAnnexMergeDir, and later
|
|
||||||
- mergeDirectCleanup handles updating the work tree.
|
|
||||||
-}
|
|
||||||
resolveMerge :: Git.Ref -> Annex Bool
|
|
||||||
resolveMerge branch = do
|
|
||||||
top <- fromRepo Git.repoPath
|
|
||||||
(fs, cleanup) <- inRepo (LsFiles.unmerged [top])
|
|
||||||
mergedfs <- catMaybes <$> mapM (resolveMerge' branch) fs
|
|
||||||
let merged = not (null mergedfs)
|
|
||||||
void $ liftIO cleanup
|
|
||||||
|
|
||||||
unlessM isDirect $ do
|
|
||||||
(deleted, cleanup2) <- inRepo (LsFiles.deleted [top])
|
|
||||||
unless (null deleted) $
|
|
||||||
Annex.Queue.addCommand "rm" [Params "--quiet -f --"] deleted
|
|
||||||
void $ liftIO cleanup2
|
|
||||||
|
|
||||||
when merged $ do
|
|
||||||
unlessM isDirect $
|
|
||||||
cleanConflictCruft mergedfs top
|
|
||||||
Annex.Queue.flush
|
|
||||||
void $ inRepo $ Git.Command.runBool
|
|
||||||
[ Param "commit"
|
|
||||||
, Param "-m"
|
|
||||||
, Param "git-annex automatic merge conflict fix"
|
|
||||||
]
|
|
||||||
showLongNote "Merge conflict was automatically resolved; you may want to examine the result."
|
|
||||||
return merged
|
|
||||||
|
|
||||||
resolveMerge' :: Git.Ref -> LsFiles.Unmerged -> Annex (Maybe FilePath)
|
|
||||||
resolveMerge' branch u
|
|
||||||
| mergeable LsFiles.valUs && mergeable LsFiles.valThem = do
|
|
||||||
kus <- getKey LsFiles.valUs
|
|
||||||
kthem <- getKey LsFiles.valThem
|
|
||||||
case (kus, kthem) of
|
|
||||||
-- Both sides of conflict are annexed files
|
|
||||||
(Just keyUs, Just keyThem) -> do
|
|
||||||
unstageoldfile
|
|
||||||
if keyUs == keyThem
|
|
||||||
then makelink keyUs
|
|
||||||
else do
|
|
||||||
makelink keyUs
|
|
||||||
makelink keyThem
|
|
||||||
return $ Just file
|
|
||||||
-- Our side is annexed, other side is not.
|
|
||||||
(Just keyUs, Nothing) -> do
|
|
||||||
unstageoldfile
|
|
||||||
whenM isDirect $
|
|
||||||
stagefromdirectmergedir file
|
|
||||||
makelink keyUs
|
|
||||||
return $ Just file
|
|
||||||
-- Our side is not annexed, other side is.
|
|
||||||
(Nothing, Just keyThem) -> do
|
|
||||||
unstageoldfile
|
|
||||||
makelink keyThem
|
|
||||||
return $ Just file
|
|
||||||
-- Neither side is annexed; cannot resolve.
|
|
||||||
(Nothing, Nothing) -> return Nothing
|
|
||||||
| otherwise = return Nothing
|
|
||||||
where
|
|
||||||
file = LsFiles.unmergedFile u
|
|
||||||
mergeable select = select (LsFiles.unmergedBlobType u)
|
|
||||||
`elem` [Just SymlinkBlob, Nothing]
|
|
||||||
makelink key = do
|
|
||||||
let dest = variantFile file key
|
|
||||||
l <- inRepo $ gitAnnexLink dest key
|
|
||||||
ifM isDirect
|
|
||||||
( do
|
|
||||||
d <- fromRepo gitAnnexMergeDir
|
|
||||||
replaceFile (d </> dest) $ makeAnnexLink l
|
|
||||||
, replaceFile dest $ makeAnnexLink 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
|
|
||||||
unstageoldfile = Annex.Queue.addCommand "rm" [Params "--quiet -f --cached --"] [file]
|
|
||||||
|
|
||||||
{- stage an item from the direct mode merge directory, which may
|
|
||||||
- be a directory with arbitrary contents -}
|
|
||||||
stagefromdirectmergedir item = Annex.Queue.addUpdateIndex
|
|
||||||
=<< fromRepo (UpdateIndex.lsSubTree branch item)
|
|
||||||
|
|
||||||
{- git-merge moves conflicting files away to files
|
|
||||||
- named something like f~HEAD or f~branch, but the
|
|
||||||
- exact name chosen can vary. Once the conflict is resolved,
|
|
||||||
- this cruft can be deleted. To avoid deleting legitimate
|
|
||||||
- files that look like this, only delete files that are
|
|
||||||
- A) not staged in git and B) look like git-annex symlinks.
|
|
||||||
-}
|
|
||||||
cleanConflictCruft :: [FilePath] -> FilePath -> Annex ()
|
|
||||||
cleanConflictCruft resolvedfs top = do
|
|
||||||
(fs, cleanup) <- inRepo $ LsFiles.notInRepo False [top]
|
|
||||||
mapM_ clean fs
|
|
||||||
void $ liftIO cleanup
|
|
||||||
where
|
|
||||||
clean f
|
|
||||||
| matchesresolved f = whenM (isJust <$> isAnnexLink f) $
|
|
||||||
liftIO $ nukeFile f
|
|
||||||
| otherwise = noop
|
|
||||||
s = S.fromList resolvedfs
|
|
||||||
matchesresolved f = S.member (base f) s
|
|
||||||
base f = reverse $ drop 1 $ dropWhile (/= '~') $ reverse f
|
|
||||||
|
|
||||||
changed :: Remote -> Git.Ref -> Annex Bool
|
changed :: Remote -> Git.Ref -> Annex Bool
|
||||||
changed remote b = do
|
changed remote b = do
|
||||||
let r = remoteBranch remote b
|
let r = remoteBranch remote b
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue