fixed the double commits problem

This commit is contained in:
Joey Hess 2012-06-10 18:29:05 -04:00
parent fc0dd79774
commit aae0ba1995
2 changed files with 84 additions and 59 deletions

View file

@ -94,9 +94,8 @@ undo file key e = do
src <- inRepo $ gitAnnexLocation key
liftIO $ moveFile src file
{- Creates the symlink to the annexed content, and also returns the link's
- text. -}
link :: FilePath -> Key -> Bool -> Annex FilePath
{- Creates the symlink to the annexed content. -}
link :: FilePath -> Key -> Bool -> Annex ()
link file key hascontent = handle (undo file key) $ do
l <- calcGitLink file key
liftIO $ createSymbolicLink l file
@ -110,8 +109,6 @@ link file key hascontent = handle (undo file key) $ do
mtime <- modificationTime <$> getFileStatus file
touch file (TimeSpec mtime) False
return l
{- Note: Several other commands call this, and expect it to
- create the symlink and add it. -}
cleanup :: FilePath -> Key -> Bool -> CommandCleanup

View file

@ -30,7 +30,16 @@ import Utility.Inotify
import System.INotify
#endif
type ChangeChan = TChan UTCTime
type ChangeChan = TChan Change
type Handler = FilePath -> Annex (Maybe Change)
data Change = Change
{ changeTime :: UTCTime
, changeFile :: FilePath
, changeDesc :: String
}
deriving (Show)
def :: [Command]
def = [command "watch" paramPaths seek "watch for changes"]
@ -52,7 +61,7 @@ watch = do
withStateMVar $ \st -> liftIO $ withINotify $ \i -> do
changechan <- atomically newTChan
_ <- forkIO $ commitThread st changechan
let hook a = Just $ runHook st changechan a
let hook a = Just $ runHandler st changechan a
let hooks = WatchHooks
{ addHook = hook onAdd
, delHook = hook onDel
@ -94,56 +103,70 @@ runStateMVar mvar a = do
!newstate <- Annex.exec startstate a
putMVar mvar newstate
{- Runs a hook, inside the Annex monad.
{- Runs an action handler, inside the Annex monad.
-
- Exceptions are ignored, otherwise a whole watcher thread could be crashed.
-}
runHook :: MVar Annex.AnnexState -> ChangeChan -> (FilePath -> Annex ()) -> FilePath -> IO ()
runHook st changetimes a f = handle =<< tryIO (runStateMVar st go)
runHandler :: MVar Annex.AnnexState -> ChangeChan -> Handler -> FilePath -> IO ()
runHandler st changechan hook file = handle =<< tryIO (runStateMVar st go)
where
go = do
a f
signalChange changetimes
go = maybe noop (signalChange changechan) =<< hook file
handle (Right ()) = return ()
handle (Left e) = putStrLn $ show e
{- Handlers call this when they made a change that needs to get committed. -}
madeChange :: FilePath -> String -> Annex (Maybe Change)
madeChange file desc = liftIO $
Just <$> (Change <$> getCurrentTime <*> pure file <*> pure desc)
{- Adding a file is tricky; the file has to be replaced with a symlink
- but this is race prone, as the symlink could be changed immediately
- after creation. To avoid that race, git add is not used to stage the
- symlink. -}
onAdd :: FilePath -> Annex ()
- symlink.
-
- Inotify will notice the new symlink, so this Handler does not stage it
- or return a Change, leaving that to onAddSymlink.
-}
onAdd :: Handler
onAdd file = do
showStart "add" file
Command.Add.ingest file >>= go
handle =<< Command.Add.ingest file
return Nothing
where
go Nothing = showEndFail
go (Just key) = do
link <- Command.Add.link file key True
stageSymlink file link
handle Nothing = showEndFail
handle (Just key) = do
Command.Add.link file key True
showEndOk
{- A symlink might be an arbitrary symlink, which is just added.
- Or, if it is a git-annex symlink, ensure it points to the content
- before adding it.
-}
onAddSymlink :: FilePath -> Annex ()
onAddSymlink :: Handler
onAddSymlink file = go =<< Backend.lookupFile file
where
go Nothing = addlink =<< liftIO (readSymbolicLink file)
go Nothing = do
addlink =<< liftIO (readSymbolicLink file)
madeChange file "add"
go (Just (key, _)) = do
link <- calcGitLink file key
ifM ((==) link <$> liftIO (readSymbolicLink file))
( addlink link
( do
addlink link
madeChange file "add"
, do
liftIO $ removeFile file
liftIO $ createSymbolicLink link file
addlink link
madeChange file "fix"
)
addlink link = stageSymlink file link
onDel :: FilePath -> Annex ()
onDel file = Annex.Queue.addUpdateIndex =<<
inRepo (Git.UpdateIndex.unstageFile file)
onDel :: Handler
onDel file = do
Annex.Queue.addUpdateIndex =<<
inRepo (Git.UpdateIndex.unstageFile file)
madeChange file "rm"
{- A directory has been deleted, or moved, so tell git to remove anything
- that was inside it from its cache. Since it could reappear at any time,
@ -152,13 +175,17 @@ onDel file = Annex.Queue.addUpdateIndex =<<
- Note: This could use unstageFile, but would need to run another git
- command to get the recursive list of files in the directory, so rm is
- just as good. -}
onDelDir :: FilePath -> Annex ()
onDelDir dir = Annex.Queue.addCommand "rm"
[Params "--quiet -r --cached --ignore-unmatch --"] [dir]
onDelDir :: Handler
onDelDir dir = do
Annex.Queue.addCommand "rm"
[Params "--quiet -r --cached --ignore-unmatch --"] [dir]
madeChange dir "rmdir"
{- Called when there's an error with inotify. -}
onErr :: String -> Annex ()
onErr = warning
onErr :: Handler
onErr msg = do
warning msg
return Nothing
{- Adds a symlink to the index, without ever accessing the actual symlink
- on disk. -}
@ -168,16 +195,17 @@ stageSymlink file linktext =
inRepo (Git.UpdateIndex.stageSymlink file linktext)
{- Signals that a change has been made, that needs to get committed. -}
signalChange :: ChangeChan -> Annex ()
signalChange chan = do
liftIO $ (atomically . writeTChan chan) =<< getCurrentTime
signalChange :: ChangeChan -> Change -> Annex ()
signalChange chan change = do
liftIO $ atomically $ writeTChan chan change
-- Just in case the commit thread is not flushing
-- the queue fast enough.
Annex.Queue.flushWhenFull
{- Gets the times of all unhandled changes.
{- Gets all unhandled changes.
- Blocks until at least one change is made. -}
getChanges :: ChangeChan -> IO [UTCTime]
getChanges :: ChangeChan -> IO [Change]
getChanges chan = atomically $ do
c <- readTChan chan
go [c]
@ -190,10 +218,10 @@ getChanges chan = atomically $ do
{- Puts unhandled changes back into the channel.
- Note: Original order is not preserved. -}
refillChanges :: ChangeChan -> [UTCTime] -> IO ()
refillChanges :: ChangeChan -> [Change] -> IO ()
refillChanges chan cs = atomically $ mapM_ (writeTChan chan) cs
{- This thread makes git commits. -}
{- This thread makes git commits at appropriate times. -}
commitThread :: MVar Annex.AnnexState -> ChangeChan -> IO ()
commitThread st changechan = forever $ do
-- First, a simple rate limiter.
@ -203,38 +231,38 @@ commitThread st changechan = forever $ do
-- Now see if now's a good time to commit.
time <- getCurrentTime
if shouldCommit time cs
then commit
then void $ tryIO $ runStateMVar st $ commitStaged
else refillChanges changechan cs
where
commit = void $ tryIO $ runStateMVar st $ do
Annex.Queue.flush
inRepo $ Git.Command.run "commit"
[ Param "--allow-empty-message"
, Param "-m", Param ""
-- Empty commits may be made if tree
-- changes cancel each other out, etc
, Param "--allow-empty"
-- Avoid running the usual git-annex
-- pre-commit hook; watch does the same
-- symlink fixing, and we don't want to
-- deal with unlocked files in these
-- commits.
, Param "--quiet"
]
oneSecond = 1000000 -- microseconds
commitStaged :: Annex ()
commitStaged = do
Annex.Queue.flush
inRepo $ Git.Command.run "commit"
[ Param "--allow-empty-message"
, Param "-m", Param ""
-- Empty commits may be made if tree changes cancel
-- each other out, etc
, Param "--allow-empty"
-- Avoid running the usual git-annex pre-commit hook;
-- watch does the same symlink fixing, and we don't want
-- to deal with unlocked files in these commits.
, Param "--quiet"
]
{- Decide if now is a good time to make a commit.
- Note that the list of change times has an undefined order.
-
- Current strategy: If there have been 10 commits within the past second,
- a batch activity is taking place, so wait for later.
-}
shouldCommit :: UTCTime -> [UTCTime] -> Bool
shouldCommit now changetimes
shouldCommit :: UTCTime -> [Change] -> Bool
shouldCommit now changes
| len == 0 = False
| len > 4096 = True -- avoid bloating queue too much
| length (filter thisSecond changetimes) < 10 = True
| length (filter thisSecond changes) < 10 = True
| otherwise = False -- batch activity
where
len = length changetimes
thisSecond t = now `diffUTCTime` t <= 1
len = length changes
thisSecond c = now `diffUTCTime` changeTime c <= 1