summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Joey Hess <joeyh@joeyh.name>2016-04-09 14:12:25 -0400
committerGravatar Joey Hess <joeyh@joeyh.name>2016-04-09 14:12:25 -0400
commitdcf717f6055c21cc4ad05d692de8c7884a8c7489 (patch)
tree8cf6c1ad7dd348d97db368dd3237bd3f1b36f160
parentd42acd3edd6a07abb236a86bce523168b78c5e80 (diff)
fix master push overwrite race when updating adjusted branch, by maintaining basis ref
-rw-r--r--Annex/AdjustedBranch.hs112
-rw-r--r--doc/design/adjusted_branches.mdwn40
2 files changed, 95 insertions, 57 deletions
diff --git a/Annex/AdjustedBranch.hs b/Annex/AdjustedBranch.hs
index 3191e58a8..c92c528c1 100644
--- a/Annex/AdjustedBranch.hs
+++ b/Annex/AdjustedBranch.hs
@@ -100,6 +100,17 @@ adjustTreeItem ShowMissingAdjustment ti = return (Just ti)
type OrigBranch = Branch
type AdjBranch = Branch
+-- This is a hidden branch ref, that's used as the basis for the AdjBranch,
+-- since pushes can overwrite the OrigBranch at any time. So, changes
+-- are propigated from the AdjBranch to the head of the BasisBranch.
+newtype BasisBranch = BasisBranch Ref
+
+-- The basis for refs/heads/adjusted/master(unlocked) is
+-- refs/adjusted/master(unlocked).
+basisBranch :: AdjBranch -> BasisBranch
+basisBranch adjbranch = BasisBranch $
+ Ref ("refs/" ++ fromRef (Git.Ref.base adjbranch))
+
adjustedBranchPrefix :: String
adjustedBranchPrefix = "refs/heads/adjusted/"
@@ -135,7 +146,7 @@ adjustedToOriginal b
getAdjustment :: Branch -> Maybe Adjustment
getAdjustment = fmap fst . adjustedToOriginal
-fromAdjustedBranch :: Branch -> OrigBranch
+fromAdjustedBranch :: AdjBranch -> OrigBranch
fromAdjustedBranch b = maybe b snd (adjustedToOriginal b)
originalBranch :: Annex (Maybe OrigBranch)
@@ -143,7 +154,7 @@ originalBranch = fmap fromAdjustedBranch <$> inRepo Git.Branch.current
{- Enter an adjusted version of current branch (or, if already in an
- adjusted version of a branch, changes the adjustment of the original
- t a- branch).
+ - branch).
-
- Can fail, if no branch is checked out, or perhaps if staged changes
- conflict with the adjusted branch.
@@ -173,23 +184,30 @@ adjustToCrippledFileSystem = do
]
enterAdjustedBranch UnlockAdjustment
+updateBasisBranch :: BasisBranch -> Ref -> Annex ()
+updateBasisBranch (BasisBranch basis) new =
+ inRepo $ Git.Branch.update' basis new
+
adjustBranch :: Adjustment -> OrigBranch -> Annex AdjBranch
adjustBranch adj origbranch = do
- sha <- adjust adj origbranch
+ -- Start basis off with the current value of the origbranch.
+ updateBasisBranch basis origbranch
+ sha <- adjustCommit adj basis
inRepo $ Git.Branch.update "entering adjusted branch" adjbranch sha
return adjbranch
where
adjbranch = originalToAdjusted origbranch adj
+ basis = basisBranch adjbranch
-adjust :: Adjustment -> Ref -> Annex Sha
-adjust adj orig = do
- treesha <- adjustTree adj orig
- commitAdjustedTree treesha orig
+adjustCommit :: Adjustment -> BasisBranch -> Annex Sha
+adjustCommit adj basis = do
+ treesha <- adjustTree adj basis
+ commitAdjustedTree treesha basis
-adjustTree :: Adjustment -> Ref -> Annex Sha
-adjustTree adj orig = do
+adjustTree :: Adjustment -> BasisBranch -> Annex Sha
+adjustTree adj (BasisBranch basis) = do
let toadj = adjustTreeItem adj
- treesha <- Git.Tree.adjustTree toadj [] [] orig =<< Annex.gitRepo
+ treesha <- Git.Tree.adjustTree toadj [] [] basis =<< Annex.gitRepo
return treesha
type CommitsPrevented = Git.LockFile.LockHandle
@@ -213,11 +231,13 @@ preventCommits = bracket setup cleanup
- clones of a repo, at different times. The commit message and other
- metadata is based on the parent.
-}
-commitAdjustedTree :: Sha -> Ref -> Annex Sha
-commitAdjustedTree treesha parent = commitAdjustedTree' treesha parent [parent]
+commitAdjustedTree :: Sha -> BasisBranch -> Annex Sha
+commitAdjustedTree treesha parent@(BasisBranch b) =
+ commitAdjustedTree' treesha parent [b]
-commitAdjustedTree' :: Sha -> Ref -> [Ref] -> Annex Sha
-commitAdjustedTree' treesha basis parents = go =<< catCommit basis
+commitAdjustedTree' :: Sha -> BasisBranch -> [Ref] -> Annex Sha
+commitAdjustedTree' treesha (BasisBranch basis) parents =
+ go =<< catCommit basis
where
go Nothing = inRepo mkcommit
go (Just basiscommit) = inRepo $ commitWithMetaData
@@ -238,10 +258,15 @@ updateAdjustedBranch tomerge (origbranch, adj) commitmode = catchBoolIO $
join $ preventCommits $ \commitsprevented ->
go commitsprevented =<< inRepo Git.Branch.current
where
+ adjbranch = originalToAdjusted origbranch adj
+ basis = basisBranch adjbranch
+
go commitsprevented (Just currbranch) =
ifM (inRepo $ Git.Branch.changed currbranch tomerge)
( do
- (updatedorig, _) <- propigateAdjustedCommits' origbranch (adj, currbranch) commitsprevented
+ (updatedorig, _) <- propigateAdjustedCommits'
+ origbranch (adj, currbranch)
+ commitsprevented
changestomerge updatedorig currbranch
, nochangestomerge
)
@@ -255,7 +280,7 @@ updateAdjustedBranch tomerge (origbranch, adj) commitmode = catchBoolIO $
- updatedorig. The result of the merge can the be
- adjusted to yield the final adjusted branch.
-
- - In order to do a merge into a branch that is not checked out,
+ - In order to do a merge into a ref that is not checked out,
- set the work tree to a temp directory, and set GIT_DIR
- to another temp directory, in which HEAD contains the
- updatedorig sha. GIT_COMMON_DIR is set to point to the real
@@ -293,21 +318,23 @@ updateAdjustedBranch tomerge (origbranch, adj) commitmode = catchBoolIO $
createDirectoryIfMissing True d
cleanup _ = removeDirectoryRecursive d
- {- A merge commit has been made between the origbranch and
- - tomerge. Update origbranch to point to that commit, adjust
- - it to get the new adjusted branch, and check it out.
+ {- A merge commit has been made between the basisbranch and
+ - tomerge. Update the basisbranch and origbranch to point
+ - to that commit, adjust it to get the new adjusted branch,
+ - and check it out.
-
- But, there may be unstaged work tree changes that conflict,
- so the check out is done by making a normal merge of
- the new adjusted branch.
-}
postmerge currbranch (Just mergecommit) = do
- inRepo $ Git.Branch.update "updating original branch" origbranch mergecommit
- adjtree <- adjustTree adj mergecommit
- adjmergecommit <- commitAdjustedTree adjtree mergecommit
+ updateBasisBranch basis mergecommit
+ inRepo $ Git.Branch.update' origbranch mergecommit
+ adjtree <- adjustTree adj (BasisBranch mergecommit)
+ adjmergecommit <- commitAdjustedTree adjtree (BasisBranch mergecommit)
-- Make currbranch be the parent, so that merging
-- this commit will be a fast-forward.
- adjmergecommitff <- commitAdjustedTree' adjtree mergecommit [currbranch]
+ adjmergecommitff <- commitAdjustedTree' adjtree (BasisBranch mergecommit) [currbranch]
showAction "Merging into adjusted branch"
ifM (autoMergeFrom adjmergecommitff (Just currbranch) commitmode)
( reparent currbranch adjtree adjmergecommit =<< getcurrentcommit
@@ -337,25 +364,26 @@ updateAdjustedBranch tomerge (origbranch, adj) commitmode = catchBoolIO $
Just c -> catCommit c
{- Check for any commits present on the adjusted branch that have not yet
- - been propigated to the orig branch, and propigate them.
+ - been propigated to the basis branch, and propigate them to the basis
+ - branch and from there on to the orig branch.
-
- - After propigating the commits back to the orig banch,
- - rebase the adjusted branch on top of the updated orig branch.
+ - After propigating the commits back to the basis banch,
+ - rebase the adjusted branch on top of the updated basis branch.
-}
propigateAdjustedCommits :: OrigBranch -> (Adjustment, AdjBranch) -> Annex ()
propigateAdjustedCommits origbranch (adj, currbranch) =
preventCommits $ \commitsprevented ->
join $ snd <$> propigateAdjustedCommits' origbranch (adj, currbranch) commitsprevented
-{- Returns sha of updated orig branch, and action which will rebase
- - the adjusted branch on top of the updated orig branch. -}
+{- Returns sha of updated basis branch, and action which will rebase
+ - the adjusted branch on top of the updated basis branch. -}
propigateAdjustedCommits'
:: OrigBranch
-> (Adjustment, AdjBranch)
-> CommitsPrevented
-> Annex (Maybe Sha, Annex ())
propigateAdjustedCommits' origbranch (adj, currbranch) _commitsprevented = do
- ov <- inRepo $ Git.Ref.sha (Git.Ref.under "refs/heads" origbranch)
+ ov <- inRepo $ Git.Ref.sha basis
case ov of
Just origsha -> do
cv <- catCommit currbranch
@@ -373,12 +401,15 @@ propigateAdjustedCommits' origbranch (adj, currbranch) _commitsprevented = do
Nothing -> return (Nothing, return ())
Nothing -> return (Nothing, return ())
where
- newcommits = inRepo $ Git.Branch.changedCommits origbranch currbranch
+ (BasisBranch basis) = basisBranch adjbranch
+ adjbranch = originalToAdjusted origbranch adj
+ newcommits = inRepo $ Git.Branch.changedCommits basis currbranch
-- Get commits oldest first, so they can be processed
-- in order made.
[Param "--reverse"]
go parent _ [] = do
- inRepo $ Git.Branch.update "updating adjusted branch" origbranch parent
+ updateBasisBranch (BasisBranch basis) parent
+ inRepo $ Git.Branch.update' origbranch parent
return (Right parent)
go parent pastadjcommit (sha:l) = do
mc <- catCommit sha
@@ -393,10 +424,9 @@ propigateAdjustedCommits' origbranch (adj, currbranch) _commitsprevented = do
Right commit -> go commit pastadjcommit l
_ -> go parent pastadjcommit l
rebase currcommit newparent = do
- -- Reuse the current adjusted tree,
- -- and reparent it on top of the new
- -- version of the origbranch.
- commitAdjustedTree (commitTree currcommit) newparent
+ -- Reuse the current adjusted tree, and reparent it
+ -- on top of the newparent.
+ commitAdjustedTree (commitTree currcommit) (BasisBranch newparent)
>>= inRepo . Git.Branch.update rebaseOnTopMsg currbranch
rebaseOnTopMsg :: String
@@ -462,14 +492,18 @@ diffTreeToTreeItem dti = TreeItem
{- Cloning a repository that has an adjusted branch checked out will
- result in the clone having the same adjusted branch checked out -- but
- - the origbranch won't exist in the clone. Create the origbranch. -}
+ - the origbranch won't exist in the clone, nor will the basis.
+ - Create them. -}
checkAdjustedClone :: Annex ()
checkAdjustedClone = go =<< inRepo Git.Branch.current
where
go Nothing = return ()
go (Just currbranch) = case adjustedToOriginal currbranch of
Nothing -> return ()
- Just (_adj, origbranch) ->
- unlessM (inRepo $ Git.Ref.exists origbranch) $ do
- let remotebranch = Git.Ref.underBase "refs/remotes/origin" origbranch
+ Just (adj, origbranch) -> do
+ let remotebranch = Git.Ref.underBase "refs/remotes/origin" origbranch
+ let basis@(BasisBranch bb) = basisBranch (originalToAdjusted origbranch adj)
+ unlessM (inRepo $ Git.Ref.exists bb) $
+ updateBasisBranch basis remotebranch
+ unlessM (inRepo $ Git.Ref.exists origbranch) $
inRepo $ Git.Branch.update' origbranch remotebranch
diff --git a/doc/design/adjusted_branches.mdwn b/doc/design/adjusted_branches.mdwn
index 75ece6e99..49b9e9dd7 100644
--- a/doc/design/adjusted_branches.mdwn
+++ b/doc/design/adjusted_branches.mdwn
@@ -256,6 +256,28 @@ in [[todo/deferred_update_mode]]. Arguably, it's just as confusing for the
file to remain visible but have its content temporarily replaced with a
annex pointer.
+### master push overwrite race (fixed)
+
+There are potentially races in code that assumes a branch like
+master is not being changed by someone else.
+
+In particular, if propigateAdjustedCommits rebases the adjusted branch on
+top of master. That is called by sync. The assumption is that any changes
+in master have already been handled by updateAdjustedBranch. But, if
+another remote pushed a new master at just the right time, the adjusted
+branch could be rebased on top of a master that it doesn't incorporate,
+which is wrong.
+
+Best fix seems to be to maintain a basis ref, that is not a branch,
+like refs/adjusted/master(unlocked). Copy master's ref to it when
+entering the view branch. Then, make all adjustments via the basis
+ref, and propigate back to refs/heads/master.
+
+It's fine to overwrite changes that were pushed to master when
+propigating from the adjusted branch. Synced changes also go to
+synced/master so won't be lost. Pushes not made using git-annex sync
+of master are not really desired, just a possibility.
+
## integration with view branches
Entering a view from an adjusted branch should probably carry the adjusting
@@ -279,21 +301,3 @@ into adjusted view worktrees.]
will make copies of the content of annexed files, so this would need
to checkout the adjusted branch some other way. Maybe generalize so this
more efficient checkout is available as a git-annex command?
-* There are potentially races in code that assumes a branch like
- master is not being changed by someone else.
-
- In particular, propigateAdjustedCommits rebases the adjusted branch on
- top of master. That is called by sync. The assumption is that any changes
- in master have already been handled by updateAdjustedBranch. But, if
- another remote pushed a new master at just the right time, the adjusted
- branch could be rebased on top of a master that it doesn't incorporate,
- which is wrong.
-
- Best fix seems to be to use a hidden ref, like refs/annex/adjusted/master
- and copy master's ref to it when entering the view branch. Then, make
- all adjustments via that ref, and propigate back to refs/heads/master.
-
- It's fine to overwrite changes that were pushed to master when
- propigating from the adjusted branch. Synced changes also go to
- synced/master so won't be lost. Pushes not made using git-annex sync
- of master are not really desired, just a possibility.