diff --git a/Annex.hs b/Annex.hs
--- a/Annex.hs
+++ b/Annex.hs
@@ -34,7 +34,6 @@
 
 import "mtl" Control.Monad.Reader
 import "MonadCatchIO-transformers" Control.Monad.CatchIO
-import System.Posix.Types (Fd)
 import Control.Concurrent
 
 import Common
@@ -58,6 +57,7 @@
 import Types.UUID
 import Types.FileMatcher
 import Types.NumCopies
+import Types.LockPool
 import qualified Utility.Matcher
 import qualified Data.Map as M
 import qualified Data.Set as S
@@ -106,7 +106,7 @@
 	, trustmap :: Maybe TrustMap
 	, groupmap :: Maybe GroupMap
 	, ciphers :: M.Map StorableCipher Cipher
-	, lockpool :: M.Map FilePath Fd
+	, lockpool :: LockPool
 	, flags :: M.Map String Bool
 	, fields :: M.Map String String
 	, cleanup :: M.Map String (Annex ())
diff --git a/Annex/Content.hs b/Annex/Content.hs
--- a/Annex/Content.hs
+++ b/Annex/Content.hs
@@ -1,6 +1,6 @@
 {- git-annex file content managing
  -
- - Copyright 2010-2013 Joey Hess <joey@kitenet.net>
+ - Copyright 2010-2014 Joey Hess <joey@kitenet.net>
  -
  - Licensed under the GNU GPL version 3 or higher.
  -}
@@ -57,6 +57,10 @@
 import Annex.ReplaceFile
 import Annex.Exception
 
+#ifdef mingw32_HOST_OS
+import Utility.WinLock
+#endif
+
 {- Checks if a given key's content is currently present. -}
 inAnnex :: Key -> Annex Bool
 inAnnex key = inAnnexCheck key $ liftIO . doesFileExist
@@ -90,60 +94,105 @@
 {- A safer check; the key's content must not only be present, but
  - is not in the process of being removed. -}
 inAnnexSafe :: Key -> Annex (Maybe Bool)
-inAnnexSafe = inAnnex' (fromMaybe False) (Just False) go
+inAnnexSafe key = inAnnex' (fromMaybe False) (Just False) go key
   where
-	go f = liftIO $ openforlock f >>= check
+	is_locked = Nothing
+	is_unlocked = Just True
+	is_missing = Just False
+
+	go contentfile = maybe (checkindirect contentfile) (checkdirect contentfile)
+		=<< contentLockFile key
+
 #ifndef mingw32_HOST_OS
+	checkindirect f = liftIO $ openforlock f >>= check is_missing
+	{- In direct mode, the content file must exist, but
+	 - the lock file often generally won't exist unless a removal is in
+	 - process. This does not create the lock file, it only checks for
+	 - it. -}
+	checkdirect contentfile lockfile = liftIO $
+		ifM (doesFileExist contentfile)
+			( openforlock lockfile >>= check is_unlocked
+			, return is_missing
+			)
 	openforlock f = catchMaybeIO $
 		openFd f ReadOnly Nothing defaultFileFlags
-#else
-	openforlock _ = return $ Just ()
-#endif
-	check Nothing = return is_missing
-#ifndef mingw32_HOST_OS
-	check (Just h) = do
+	check _ (Just h) = do
 		v <- getLock h (ReadLock, AbsoluteSeek, 0, 0)
 		closeFd h
 		return $ case v of
 			Just _ -> is_locked
 			Nothing -> is_unlocked
+	check def Nothing = return def
 #else
-	check (Just _) = return is_unlocked
-#endif
-#ifndef mingw32_HOST_OS
-	is_locked = Nothing
+	checkindirect _ = return is_missing
+	{- In Windows, see if we can take a shared lock. If so, 
+	 - remove the lock file to clean up after ourselves. -}
+	checkdirect contentfile lockfile =
+		ifM (liftIO $ doesFileExist contentfile)
+			( modifyContent lockfile $ liftIO $ do
+				v <- lockShared lockfile
+				case v of
+					Nothing -> return is_locked
+					Just lockhandle -> do
+						dropLock lockhandle
+						void $ tryIO $ nukeFile lockfile
+						return is_unlocked
+			, return is_missing
+			)
 #endif
-	is_unlocked = Just True
-	is_missing = Just False
 
+{- Direct mode and especially Windows has to use a separate lock
+ - file from the content, since locking the actual content file
+ - would interfere with the user's use of it. -}
+contentLockFile :: Key -> Annex (Maybe FilePath)
+contentLockFile key = ifM isDirect
+	( Just <$> calcRepo (gitAnnexContentLock key)
+	, return Nothing
+	)
+
 {- Content is exclusively locked while running an action that might remove
  - it. (If the content is not present, no locking is done.) -}
 lockContent :: Key -> Annex a -> Annex a
-#ifndef mingw32_HOST_OS
 lockContent key a = do
-	file <- calcRepo $ gitAnnexLocation key
-	bracketIO (openforlock file >>= lock) unlock (const a)
+	contentfile <- calcRepo $ gitAnnexLocation key
+	lockfile <- contentLockFile key
+	maybe noop setuplockfile lockfile
+	bracketAnnex (liftIO $ lock contentfile lockfile) (unlock lockfile) (const a)
   where
-	{- Since files are stored with the write bit disabled, have
+	alreadylocked = error "content is locked"
+	setuplockfile lockfile = modifyContent lockfile $
+		void $ liftIO $ tryIO $
+			writeFile lockfile ""
+	cleanuplockfile lockfile = modifyContent lockfile $
+		void $ liftIO $ tryIO $
+			nukeFile lockfile
+#ifndef mingw32_HOST_OS
+	lock contentfile Nothing = opencontentforlock contentfile >>= dolock
+	lock _ (Just lockfile) = openforlock lockfile >>= dolock . Just
+	{- Since content files are stored with the write bit disabled, have
 	 - to fiddle with permissions to open for an exclusive lock. -}
-	openforlock f = catchMaybeIO $ ifM (doesFileExist f)
+	opencontentforlock f = catchMaybeIO $ ifM (doesFileExist f)
 		( withModifiedFileMode f
 			(`unionFileModes` ownerWriteMode)
-			open
-		, open
+			(openforlock f)
+		, openforlock f
 		)
-	  where
-		open = openFd f ReadWrite Nothing defaultFileFlags
-	lock Nothing = return Nothing
-	lock (Just fd) = do
+	openforlock f = openFd f ReadWrite Nothing defaultFileFlags
+	dolock Nothing = return Nothing
+	dolock (Just fd) = do
 		v <- tryIO $ setLock fd (WriteLock, AbsoluteSeek, 0, 0)
 		case v of
-			Left _ -> error "content is locked"
+			Left _ -> alreadylocked
 			Right _ -> return $ Just fd
-	unlock Nothing = noop
-	unlock (Just l) = closeFd l
+	unlock mlockfile mfd = do
+		maybe noop cleanuplockfile mlockfile
+		liftIO $ maybe noop closeFd mfd
 #else
-lockContent _key a = a -- no locking for Windows!
+	lock _ (Just lockfile) = maybe alreadylocked (return . Just) =<< lockExclusive lockfile
+	lock _ Nothing = return Nothing
+	unlock mlockfile mlockhandle = do
+		liftIO $ maybe noop dropLock mlockhandle
+		maybe noop cleanuplockfile mlockfile
 #endif
 
 {- Runs an action, passing it a temporary filename to get,
diff --git a/Annex/Exception.hs b/Annex/Exception.hs
--- a/Annex/Exception.hs
+++ b/Annex/Exception.hs
@@ -14,6 +14,7 @@
 
 module Annex.Exception (
 	bracketIO,
+	bracketAnnex,
 	tryAnnex,
 	tryAnnexIO,
 	throwAnnex,
@@ -28,6 +29,9 @@
 {- Runs an Annex action, with setup and cleanup both in the IO monad. -}
 bracketIO :: IO v -> (v -> IO b) -> (v -> Annex a) -> Annex a
 bracketIO setup cleanup = M.bracket (liftIO setup) (liftIO . cleanup)
+
+bracketAnnex :: Annex v -> (v -> Annex b) -> (v -> Annex a) -> Annex a
+bracketAnnex = M.bracket
 
 {- try in the Annex monad -}
 tryAnnex :: Annex a -> Annex (Either SomeException a)
diff --git a/Annex/Journal.hs b/Annex/Journal.hs
--- a/Annex/Journal.hs
+++ b/Annex/Journal.hs
@@ -20,6 +20,10 @@
 import qualified Git
 import Annex.Perms
 
+#ifdef mingw32_HOST_OS
+import Utility.WinLock
+#endif
+
 {- Records content for a file in the branch to the journal.
  -
  - Using the journal, rather than immediatly staging content to the index
@@ -116,13 +120,8 @@
 		l <- noUmask mode $ createFile lockfile mode
 		waitToSetLock l (WriteLock, AbsoluteSeek, 0, 0)
 		return l
-#else
-	lock lockfile _mode = do
-		writeFile lockfile ""
-		return lockfile
-#endif
-#ifndef mingw32_HOST_OS
 	unlock = closeFd
 #else
-	unlock = removeFile
+	lock lockfile _mode = waitToLock $ lockExclusive lockfile
+	unlock = dropLock
 #endif
diff --git a/Annex/LockPool.hs b/Annex/LockPool.hs
--- a/Annex/LockPool.hs
+++ b/Annex/LockPool.hs
@@ -1,6 +1,6 @@
 {- git-annex lock pool
  -
- - Copyright 2012 Joey Hess <joey@kitenet.net>
+ - Copyright 2012, 2014 Joey Hess <joey@kitenet.net>
  -
  - Licensed under the GNU GPL version 3 or higher.
  -}
@@ -9,13 +9,16 @@
 
 module Annex.LockPool where
 
-import qualified Data.Map as M
-import System.Posix.Types (Fd)
-
 import Common.Annex
 import Annex
+import Types.LockPool
+
+import qualified Data.Map as M
+
 #ifndef mingw32_HOST_OS
 import Annex.Perms
+#else
+import Utility.WinLock
 #endif
 
 {- Create a specified lock file, and takes a shared lock. -}
@@ -26,31 +29,32 @@
 	go Nothing = do
 #ifndef mingw32_HOST_OS
 		mode <- annexFileMode
-		fd <- liftIO $ noUmask mode $
+		lockhandle <- liftIO $ noUmask mode $
 			openFd file ReadOnly (Just mode) defaultFileFlags
-		liftIO $ waitToSetLock fd (ReadLock, AbsoluteSeek, 0, 0)
+		liftIO $ waitToSetLock lockhandle (ReadLock, AbsoluteSeek, 0, 0)
 #else
-		liftIO $ writeFile file ""
-		let fd = 0
+		lockhandle <- liftIO $ waitToLock $ lockShared file
 #endif
-		changePool $ M.insert file fd
+		changePool $ M.insert file lockhandle
 
 unlockFile :: FilePath -> Annex ()
 unlockFile file = maybe noop go =<< fromPool file
   where
-	go fd = do
+	go lockhandle = do
 #ifndef mingw32_HOST_OS
-		liftIO $ closeFd fd
+		liftIO $ closeFd lockhandle
+#else
+		liftIO $ dropLock lockhandle
 #endif
 		changePool $ M.delete file
 
-getPool :: Annex (M.Map FilePath Fd)
+getPool :: Annex LockPool
 getPool = getState lockpool
 
-fromPool :: FilePath -> Annex (Maybe Fd)
+fromPool :: FilePath -> Annex (Maybe LockHandle)
 fromPool file = M.lookup file <$> getPool
 
-changePool :: (M.Map FilePath Fd -> M.Map FilePath Fd) -> Annex ()
+changePool :: (LockPool -> LockPool) -> Annex ()
 changePool a = do
 	m <- getPool
 	changeState $ \s -> s { lockpool = a m }
diff --git a/Assistant/Drop.hs b/Assistant/Drop.hs
--- a/Assistant/Drop.hs
+++ b/Assistant/Drop.hs
@@ -14,7 +14,7 @@
 import Assistant.DaemonStatus
 import Annex.Drop (handleDropsFrom, Reason)
 import Logs.Location
-import RunCommand
+import CmdLine.Action
 
 {- Drop from local and/or remote when allowed by the preferred content and
  - numcopies settings. -}
@@ -22,4 +22,4 @@
 handleDrops reason fromhere key f knownpresentremote = do
 	syncrs <- syncDataRemotes <$> getDaemonStatus
 	locs <- liftAnnex $ loggedLocations key
-	liftAnnex $ handleDropsFrom locs syncrs reason fromhere key f knownpresentremote callCommand
+	liftAnnex $ handleDropsFrom locs syncrs reason fromhere key f knownpresentremote callCommandAction
diff --git a/Assistant/Threads/TransferScanner.hs b/Assistant/Threads/TransferScanner.hs
--- a/Assistant/Threads/TransferScanner.hs
+++ b/Assistant/Threads/TransferScanner.hs
@@ -29,7 +29,7 @@
 import qualified Backend
 import Annex.Content
 import Annex.Wanted
-import RunCommand
+import CmdLine.Action
 
 import qualified Data.Set as S
 
@@ -159,7 +159,7 @@
 		present <- liftAnnex $ inAnnex key
 		liftAnnex $ handleDropsFrom locs syncrs
 			"expensive scan found too many copies of object"
-			present key (Just f) Nothing callCommand
+			present key (Just f) Nothing callCommandAction
 		liftAnnex $ do
 			let slocs = S.fromList locs
 			let use a = return $ mapMaybe (a key slocs) syncrs
diff --git a/CHANGELOG b/CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,12 @@
+git-annex (5.20140128) UNRELEASED; urgency=medium
+
+  * Windows: It's now safe to run multiple git-annex processes concurrently
+    on Windows; the lock files have been sorted out.
+  * Fixed direct mode annexed content locking code, which is used to
+    guard against recursive file drops.
+
+ -- Joey Hess <joeyh@debian.org>  Tue, 28 Jan 2014 13:57:19 -0400
+
 git-annex (5.20140127) unstable; urgency=medium
 
   * sync --content: New option that makes the content of annexed files be
@@ -30,6 +39,7 @@
   * repair: Check git version at run time.
   * assistant: Run the periodic git gc in batch mode.
   * added annex.secure-erase-command config option.
+  * test suite: Use tasty-rerun, and expose tasty command-line options.
   * Optimise non-bare http remotes; no longer does a 404 to the wrong
     url every time before trying the right url. Needs annex-bare to be
     set to false, which is done when initially probing the uuid of a
diff --git a/CmdLine.hs b/CmdLine.hs
--- a/CmdLine.hs
+++ b/CmdLine.hs
@@ -50,7 +50,7 @@
 				whenM (annexDebug <$> Annex.getGitConfig) $
 					liftIO enableDebugOutput
 				startup
-				performCommand cmd params
+				performCommandAction cmd params
 				shutdown $ cmdnocommit cmd
   where
 	err msg = msg ++ "\n\n" ++ usage header allcmds
diff --git a/CmdLine/Action.hs b/CmdLine/Action.hs
new file mode 100644
--- /dev/null
+++ b/CmdLine/Action.hs
@@ -0,0 +1,70 @@
+{- git-annex command-line actions
+ -
+ - Copyright 2010-2014 Joey Hess <joey@kitenet.net>
+ -
+ - Licensed under the GNU GPL version 3 or higher.
+ -}
+
+{-# LANGUAGE BangPatterns #-}
+
+module CmdLine.Action where
+
+import Common.Annex
+import qualified Annex
+import Types.Command
+import qualified Annex.Queue
+import Annex.Exception
+
+type CommandActionRunner = CommandStart -> CommandCleanup
+
+{- Runs a command, starting with the check stage, and then
+ - the seek stage. Finishes by printing the number of commandActions that
+ - failed. -}
+performCommandAction :: Command -> CmdParams -> Annex ()
+performCommandAction Command { cmdseek = seek, cmdcheck = c, cmdname = name } params = do
+	mapM_ runCheck c
+	Annex.changeState $ \s -> s { Annex.errcounter = 0 }
+	seek params
+	showerrcount =<< Annex.getState Annex.errcounter
+  where
+	showerrcount 0 = noop
+	showerrcount cnt = error $ name ++ ": " ++ show cnt ++ " failed"
+
+{- Runs one of the actions needed to perform a command.
+ - Individual actions can fail without stopping the whole command,
+ - including by throwing IO errors (but other errors terminate the whole
+ - command).
+ - 
+ - This should only be run in the seek stage. -}
+commandAction :: CommandActionRunner
+commandAction a = handle =<< tryAnnexIO go
+  where
+	go = do
+		Annex.Queue.flushWhenFull
+		callCommandAction a
+	handle (Right True) = return True
+	handle (Right False) = incerr
+	handle (Left err) = do
+		showErr err
+		showEndFail
+		incerr
+	incerr = do
+		Annex.changeState $ \s -> 
+			let ! c = Annex.errcounter s + 1 
+			    ! s' = s { Annex.errcounter = c }
+			in s'
+		return False
+
+{- Runs a single command action through the start, perform and cleanup
+ - stages, without catching errors. Useful if one command wants to run
+ - part of another command. -}
+callCommandAction :: CommandActionRunner
+callCommandAction = start
+  where
+	start   = stage $ maybe skip perform
+	perform = stage $ maybe failure cleanup
+	cleanup = stage $ status
+	stage = (=<<)
+	skip = return True
+	failure = showEndFail >> return False
+	status r = showEndResult r >> return r
diff --git a/CmdLine/Seek.hs b/CmdLine/Seek.hs
--- a/CmdLine/Seek.hs
+++ b/CmdLine/Seek.hs
@@ -23,10 +23,10 @@
 import qualified Git.LsFiles as LsFiles
 import qualified Limit
 import CmdLine.Option
+import CmdLine.Action
 import Logs.Location
 import Logs.Unused
 import Annex.CatFile
-import RunCommand
 
 withFilesInGit :: (FilePath -> CommandStart) -> CommandSeek
 withFilesInGit a params = seekActions $ prepFiltered a $
diff --git a/Command.hs b/Command.hs
--- a/Command.hs
+++ b/Command.hs
@@ -32,7 +32,7 @@
 import CmdLine.Seek as ReExported
 import Checks as ReExported
 import CmdLine.Usage as ReExported
-import RunCommand as ReExported
+import CmdLine.Action as ReExported
 import CmdLine.Option as ReExported
 import CmdLine.GitAnnex.Options as ReExported
 
diff --git a/Command/PreCommit.hs b/Command/PreCommit.hs
--- a/Command/PreCommit.hs
+++ b/Command/PreCommit.hs
@@ -31,7 +31,7 @@
 
 startIndirect :: FilePath -> CommandStart
 startIndirect file = next $ do
-	unlessM (callCommand $ Command.Add.start file) $
+	unlessM (callCommandAction $ Command.Add.start file) $
 		error $ "failed to add " ++ file ++ "; canceling commit"
 	next $ return True
 
diff --git a/Command/Sync.hs b/Command/Sync.hs
--- a/Command/Sync.hs
+++ b/Command/Sync.hs
@@ -513,7 +513,7 @@
 	-- Using callCommand rather than commandAction for drops,
 	-- because a failure to drop does not mean the sync failed.
 	handleDropsFrom (putrs ++ locs) rs "unwanted" True k (Just f)
-		Nothing callCommand
+		Nothing callCommandAction
   where
   	wantget have = allM id 
 		[ pure (not $ null have)
diff --git a/Command/WebApp.hs b/Command/WebApp.hs
--- a/Command/WebApp.hs
+++ b/Command/WebApp.hs
@@ -107,7 +107,7 @@
 		(d:_) -> do
 			setCurrentDirectory d
 			state <- Annex.new =<< Git.CurrentRepo.get
-			void $ Annex.eval state $ callCommand $
+			void $ Annex.eval state $ callCommandAction $
 				start' False listenhost
 
 {- Run the webapp without a repository, which prompts the user, makes one,
diff --git a/Locations.hs b/Locations.hs
--- a/Locations.hs
+++ b/Locations.hs
@@ -14,6 +14,7 @@
 	objectDir,
 	gitAnnexLocation,
 	gitAnnexLink,
+	gitAnnexContentLock,
 	gitAnnexMapping,
 	gitAnnexInodeCache,
 	gitAnnexInodeSentinal,
@@ -141,6 +142,12 @@
 	return $ relPathDirToFile (parentDir absfile) loc
   where
   	whoops = error $ "unable to normalize " ++ file
+
+{- File used to lock a key's content. -}
+gitAnnexContentLock :: Key -> Git.Repo -> GitConfig -> IO FilePath
+gitAnnexContentLock key r config = do
+	loc <- gitAnnexLocation key r config
+	return $ loc ++ ".lck"
 
 {- File that maps from a key to the file(s) in the git repository.
  - Used in direct mode. -}
diff --git a/Logs/Transfer.hs b/Logs/Transfer.hs
--- a/Logs/Transfer.hs
+++ b/Logs/Transfer.hs
@@ -29,6 +29,7 @@
 #else
 import System.Win32.Process (ProcessId)
 import System.Win32.Process.Current (getCurrentProcessId)
+import Utility.WinLock
 #endif
 
 #ifndef mingw32_HOST_OS
@@ -147,7 +148,7 @@
 			openFd (transferLockFile tfile) ReadWrite (Just mode)
 				defaultFileFlags { trunc = True }
 		case mfd of
-			Nothing -> return (mfd, False)
+			Nothing -> return (Nothing, False)
 			Just fd -> do
 				locked <- catchMaybeIO $
 					setLock fd (WriteLock, AbsoluteSeek, 0, 0)
@@ -158,17 +159,28 @@
 						return (mfd, False)
 #else
 	prep tfile _mode info = do
-		mfd <- catchMaybeIO $ do
-			writeFile (transferLockFile tfile) ""
-			writeTransferInfoFile info tfile
-		return (mfd, False)
+		v <- catchMaybeIO $ lockExclusive (transferLockFile tfile)
+		case v of
+			Nothing -> return (Nothing, False)
+			Just Nothing -> return (Nothing, True)
+			Just (Just lockhandle) -> do
+				void $ tryIO $ writeTransferInfoFile info tfile
+				return (Just lockhandle, False)
 #endif
 	cleanup _ Nothing = noop
-	cleanup tfile (Just fd) = do
+	cleanup tfile (Just lockhandle) = do
 		void $ tryIO $ removeFile tfile
-		void $ tryIO $ removeFile $ transferLockFile tfile
 #ifndef mingw32_HOST_OS
-		closeFd fd
+		void $ tryIO $ removeFile $ transferLockFile tfile
+		closeFd lockhandle
+#else
+		{- Windows cannot delete the lockfile until the lock
+		 - is closed. So it's possible to race with another
+		 - process that takes the lock before it's removed,
+		 - so ignore failure to remove.
+		 -}
+		dropLock lockhandle
+		void $ tryIO $ removeFile $ transferLockFile tfile
 #endif
 	retry oldinfo metervar run = do
 		v <- tryAnnex run
@@ -246,11 +258,14 @@
 				Just (pid, _) -> liftIO $ catchDefaultIO Nothing $
 					readTransferInfoFile (Just pid) tfile
 #else
-	ifM (liftIO $ doesFileExist $ transferLockFile tfile)
-		( liftIO $ catchDefaultIO Nothing $
+	v <- liftIO $ lockShared $ transferLockFile tfile
+	liftIO $ case v of
+		Nothing -> catchDefaultIO Nothing $
 			readTransferInfoFile Nothing tfile
-		, return Nothing
-		)
+		Just lockhandle -> do
+			dropLock lockhandle
+			void $ tryIO $ removeFile $ transferLockFile tfile
+			return Nothing
 #endif
 
 {- Gets all currently running transfers. -}
@@ -370,8 +385,8 @@
 	<*> pure False
   where
 #ifdef mingw32_HOST_OS
-	(firstline, rem) = separate (== '\n') s
-	(secondline, rest) = separate (== '\n') rem
+	(firstline, otherlines) = separate (== '\n') s
+	(secondline, rest) = separate (== '\n') otherlines
 	mpid' = readish secondline
 #else
 	(firstline, rest) = separate (== '\n') s
diff --git a/Logs/Unused.hs b/Logs/Unused.hs
--- a/Logs/Unused.hs
+++ b/Logs/Unused.hs
@@ -15,6 +15,8 @@
  - Licensed under the GNU GPL version 3 or higher.
  -}
 
+{-# LANGUAGE CPP #-}
+
 module Logs.Unused (
 	UnusedMap,
 	updateUnusedLog,
@@ -90,9 +92,15 @@
 readUnusedMap = log2map <$$> readUnusedLog
 
 dateUnusedLog :: FilePath -> Annex (Maybe UTCTime)
+#if MIN_VERSION_directory(1,2,0)
 dateUnusedLog prefix = do
 	f <- fromRepo $ gitAnnexUnusedLog prefix
 	liftIO $ catchMaybeIO $ getModificationTime f
+#else
+#warning foo
+-- old ghc's getModificationTime returned a ClockTime
+dateUnusedLog _prefix = return Nothing
+#endif
 
 {- Set of unused keys. This is cached for speed. -}
 unusedKeys :: Annex (S.Set Key)
diff --git a/Makefile b/Makefile
--- a/Makefile
+++ b/Makefile
@@ -54,6 +54,9 @@
 test: git-annex
 	./git-annex test
 
+retest: git-annex
+	./git-annex test --rerun-update --rerun-filter failures
+
 # hothasktags chokes on some template haskell etc, so ignore errors
 tags:
 	find . | grep -v /.git/ | grep -v /tmp/ | grep -v /dist/ | grep -v /doc/ | egrep '\.hs$$' | xargs hothasktags > tags 2>/dev/null
@@ -80,7 +83,7 @@
 		doc/.ikiwiki html dist tags Build/SysConfig.hs build-stamp \
 		Setup Build/InstallDesktopFile Build/EvilSplicer \
 		Build/Standalone Build/OSXMkLibs Build/LinuxMkLibs Build/DistributionUpdate \
-		git-union-merge
+		git-union-merge .tasty-rerun-log
 	find . -name \*.o -exec rm {} \;
 	find . -name \*.hi -exec rm {} \;
 
diff --git a/Remote/Helper/Hooks.hs b/Remote/Helper/Hooks.hs
--- a/Remote/Helper/Hooks.hs
+++ b/Remote/Helper/Hooks.hs
@@ -17,6 +17,8 @@
 import Annex.LockPool
 #ifndef mingw32_HOST_OS
 import Annex.Perms
+#else
+import Utility.WinLock
 #endif
 
 {- Modifies a remote's access functions to first run the
@@ -73,13 +75,13 @@
 		run starthook
 
 		Annex.addCleanup (remoteid ++ "-stop-command") $ runstop lck
-#ifndef mingw32_HOST_OS
 	runstop lck = do
 		-- Drop any shared lock we have, and take an
 		-- exclusive lock, without blocking. If the lock
 		-- succeeds, we're the only process using this remote,
 		-- so can stop it.
 		unlockFile lck
+#ifndef mingw32_HOST_OS
 		mode <- annexFileMode
 		fd <- liftIO $ noUmask mode $
 			openFd lck ReadWrite (Just mode) defaultFileFlags
@@ -90,5 +92,10 @@
 			Right _ -> run stophook
 		liftIO $ closeFd fd
 #else
-	runstop _lck = run stophook
+		v <- liftIO $ lockExclusive lck
+		case v of
+			Nothing -> noop
+			Just lockhandle -> do
+				run stophook
+				liftIO $ dropLock lockhandle
 #endif
diff --git a/RunCommand.hs b/RunCommand.hs
deleted file mode 100644
--- a/RunCommand.hs
+++ /dev/null
@@ -1,70 +0,0 @@
-{- git-annex running commands
- -
- - Copyright 2010-2014 Joey Hess <joey@kitenet.net>
- -
- - Licensed under the GNU GPL version 3 or higher.
- -}
-
-{-# LANGUAGE BangPatterns #-}
-
-module RunCommand where
-
-import Common.Annex
-import qualified Annex
-import Types.Command
-import qualified Annex.Queue
-import Annex.Exception
-
-type CommandActionRunner = CommandStart -> CommandCleanup
-
-{- Runs a command, starting with the check stage, and then
- - the seek stage. Finishes by printing the number of commandActions that
- - failed. -}
-performCommand :: Command -> CmdParams -> Annex ()
-performCommand Command { cmdseek = seek, cmdcheck = c, cmdname = name } params = do
-	mapM_ runCheck c
-	Annex.changeState $ \s -> s { Annex.errcounter = 0 }
-	seek params
-	showerrcount =<< Annex.getState Annex.errcounter
-  where
-	showerrcount 0 = noop
-	showerrcount cnt = error $ name ++ ": " ++ show cnt ++ " failed"
-
-{- Runs one of the actions needed to perform a command.
- - Individual actions can fail without stopping the whole command,
- - including by throwing IO errors (but other errors terminate the whole
- - command).
- - 
- - This should only be run in the seek stage. -}
-commandAction :: CommandActionRunner
-commandAction a = handle =<< tryAnnexIO go
-  where
-	go = do
-		Annex.Queue.flushWhenFull
-		callCommand a
-	handle (Right True) = return True
-	handle (Right False) = incerr
-	handle (Left err) = do
-		showErr err
-		showEndFail
-		incerr
-	incerr = do
-		Annex.changeState $ \s -> 
-			let ! c = Annex.errcounter s + 1 
-			    ! s' = s { Annex.errcounter = c }
-			in s'
-		return False
-
-{- Runs a single command action through the start, perform and cleanup
- - stages, without catching errors. Useful if one command wants to run
- - part of another command. -}
-callCommand :: CommandActionRunner
-callCommand = start
-  where
-	start   = stage $ maybe skip perform
-	perform = stage $ maybe failure cleanup
-	cleanup = stage $ status
-	stage = (=<<)
-	skip = return True
-	failure = showEndFail >> return False
-	status r = showEndResult r >> return r
diff --git a/Test.hs b/Test.hs
--- a/Test.hs
+++ b/Test.hs
@@ -13,6 +13,7 @@
 import Test.Tasty.Runners
 import Test.Tasty.HUnit
 import Test.Tasty.QuickCheck
+import Test.Tasty.Ingredients.Rerun
 import Data.Monoid
 
 import Options.Applicative hiding (command)
@@ -106,7 +107,7 @@
 
 ingredients :: [Ingredient]
 ingredients =
-	[ consoleTestReporter
+	[ rerunningTests [consoleTestReporter]
 	, listingTests
 	]
 
@@ -1269,7 +1270,7 @@
   where
 	prepare = do
 		env <- prepareTestEnv forcedirect
-		case tryIngredients ingredients mempty (initTests env) of
+		case tryIngredients [consoleTestReporter] mempty (initTests env) of
 			Nothing -> error "No tests found!?"
 			Just act -> unlessM act $
 				error "init tests failed! cannot continue"
diff --git a/Types/LockPool.hs b/Types/LockPool.hs
new file mode 100644
--- /dev/null
+++ b/Types/LockPool.hs
@@ -0,0 +1,24 @@
+{- git-annex lock pool data types
+ - 
+ - Copyright 2014 Joey Hess <joey@kitenet.net>
+ -
+ - Licensed under the GNU GPL version 3 or higher.
+ -}
+
+{-# LANGUAGE CPP #-}
+
+module Types.LockPool (
+	LockPool,
+	LockHandle
+) where
+
+import qualified Data.Map as M
+
+#ifndef mingw32_HOST_OS
+import System.Posix.Types (Fd)
+type LockHandle = Fd
+#else
+import Utility.WinLock -- defines LockHandle
+#endif
+
+type LockPool = M.Map FilePath LockHandle
diff --git a/Utility/WinLock.hs b/Utility/WinLock.hs
new file mode 100644
--- /dev/null
+++ b/Utility/WinLock.hs
@@ -0,0 +1,69 @@
+{- Windows lock files
+ -
+ - Copyright 2014 Joey Hess <joey@kitenet.net>
+ -
+ - Licensed under the GNU GPL version 3 or higher.
+ -}
+
+module Utility.WinLock (
+	lockShared,
+	lockExclusive,
+	dropLock,
+	waitToLock,
+	LockHandle
+) where
+
+import System.Win32.Types
+import System.Win32.File
+import Control.Concurrent
+
+{- Locking is exclusive, and prevents the file from being opened for read
+ - or write by any other process. So for advisory locking of a file, a
+ - different LockFile should be used. -}
+type LockFile = FilePath
+
+type LockHandle = HANDLE
+
+{- Tries to lock a file with a shared lock, which allows other processes to
+ - also lock it shared. Fails is the file is exclusively locked. -}
+lockShared :: LockFile -> IO (Maybe LockHandle)
+lockShared = openLock fILE_SHARE_READ
+
+{- Tries to take an exclusive lock on a file. Fails if another process has
+ - a shared or exclusive lock. -}
+lockExclusive :: LockFile -> IO (Maybe LockHandle)
+lockExclusive = openLock fILE_SHARE_NONE
+
+{- Windows considers just opening a file enough to lock it. This will
+ - create the LockFile if it does not already exist.
+ -
+ - Will fail if the file is already open with an incompatable ShareMode.
+ - Note that this may happen if an unrelated process, such as a virus
+ - scanner, even looks at the file. See http://support.microsoft.com/kb/316609
+ -
+ - Note that createFile busy-waits to try to avoid failing when some other
+ - process briefly has a file open. But that would make checking locks
+ - much more expensive, so is not done here. Thus, the use of c_CreateFile.
+ -}
+openLock :: ShareMode -> LockFile -> IO (Maybe LockHandle)
+openLock sharemode f = do
+	h <- withTString f $ \c_f ->
+		c_CreateFile c_f gENERIC_READ sharemode (maybePtr Nothing)
+			oPEN_ALWAYS fILE_ATTRIBUTE_NORMAL (maybePtr Nothing)
+	return $ if h == iNVALID_HANDLE_VALUE
+		then Nothing
+		else Just h
+
+dropLock :: LockHandle -> IO ()
+dropLock = closeHandle
+
+{- If the initial lock fails, this is a BUSY wait, and does not
+ - guarentee FIFO order of waiters. In other news, Windows is a POS. -}
+waitToLock :: IO (Maybe LockHandle) -> IO LockHandle
+waitToLock locker = takelock
+  where
+	takelock = go =<< locker
+	go (Just lck) = return lck
+	go Nothing = do
+		threadDelay (500000) -- half a second
+		takelock
diff --git a/debian/changelog b/debian/changelog
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,12 @@
+git-annex (5.20140128) UNRELEASED; urgency=medium
+
+  * Windows: It's now safe to run multiple git-annex processes concurrently
+    on Windows; the lock files have been sorted out.
+  * Fixed direct mode annexed content locking code, which is used to
+    guard against recursive file drops.
+
+ -- Joey Hess <joeyh@debian.org>  Tue, 28 Jan 2014 13:57:19 -0400
+
 git-annex (5.20140127) unstable; urgency=medium
 
   * sync --content: New option that makes the content of annexed files be
@@ -30,6 +39,7 @@
   * repair: Check git version at run time.
   * assistant: Run the periodic git gc in batch mode.
   * added annex.secure-erase-command config option.
+  * test suite: Use tasty-rerun, and expose tasty command-line options.
   * Optimise non-bare http remotes; no longer does a 404 to the wrong
     url every time before trying the right url. Needs annex-bare to be
     set to false, which is done when initially probing the uuid of a
diff --git a/debian/control b/debian/control
--- a/debian/control
+++ b/debian/control
@@ -54,6 +54,7 @@
 	libghc-tasty-dev (>= 0.7) [!mipsel !sparc],
 	libghc-tasty-hunit-dev [!mipsel !sparc],
 	libghc-tasty-quickcheck-dev [!mipsel !sparc],
+	libghc-tasty-rerun-dev [!mipsel !sparc],
 	libghc-optparse-applicative-dev,
 	lsof [!kfreebsd-i386 !kfreebsd-amd64],
 	ikiwiki,
diff --git a/doc/bugs/Build_error:_Ambiguous_occurrence___96__callCommand__39__.mdwn b/doc/bugs/Build_error:_Ambiguous_occurrence___96__callCommand__39__.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/bugs/Build_error:_Ambiguous_occurrence___96__callCommand__39__.mdwn
@@ -0,0 +1,74 @@
+### Please describe the problem.
+
+I get the following error when building:
+
+[[!format sh """
+$ cabal install git-annex --bindir=$HOME/bin -f"-assistant -webapp -webdav -pairing -xmpp -dns"
+
+...
+
+Configuring git-annex-5.20140127...
+Building git-annex-5.20140127...
+Preprocessing executable 'git-annex' for git-annex-5.20140127...
+[  1 of 281] Compiling Utility.Dot      ( Utility/Dot.hs, dist/build/git-annex/git-annex-tmp/Utility/Dot.o )
+[  2 of 281] Compiling BuildFlags       ( BuildFlags.hs, dist/build/git-annex/git-annex-tmp/BuildFlags.o )
+[  3 of 281] Compiling Utility.Shell    ( Utility/Shell.hs, dist/build/git-annex/git-annex-tmp/Utility/Shell.o )
+
+...
+
+[111 of 281] Compiling Backend.Hash     ( Backend/Hash.hs, dist/build/git-annex/git-annex-tmp/Backend/Hash.o )
+[112 of 281] Compiling Annex.Queue      ( Annex/Queue.hs, dist/build/git-annex/git-annex-tmp/Annex/Queue.o )
+[113 of 281] Compiling RunCommand       ( RunCommand.hs, dist/build/git-annex/git-annex-tmp/RunCommand.o )
+
+RunCommand.hs:44:17:
+    Ambiguous occurrence `callCommand'
+    It could refer to either `RunCommand.callCommand',
+                             defined at RunCommand.hs:62:1
+                          or `Common.Annex.callCommand',
+                             imported from `Common.Annex' at RunCommand.hs:12:1-19
+                             (and originally defined in `System.Process')
+cabal: Error: some packages failed to install:
+git-annex-5.20140127 failed during the building phase. The exception was:
+ExitFailure 1
+"""]]
+
+### What steps will reproduce the problem?
+
+Try building the same version.
+
+### What version of git-annex are you using? On what operating system?
+
+Building git-annex-5.20140127...
+
+[[!format sh """
+$ cabal --version
+cabal-install version 0.14.0
+using version 1.14.0 of the Cabal library
+
+$ ghc --version
+The Glorious Glasgow Haskell Compilation System, version 7.4.1
+
+$ lsb_release -a 
+No LSB modules are available.
+Distributor ID:	Ubuntu
+Description:	Ubuntu 12.04.3 LTS
+Release:	12.04
+Codename:	precise
+
+$ uname -a
+Linux sahnlpt0116 3.2.0-58-generic #88-Ubuntu SMP Tue Dec 3 17:37:58 UTC 2013 x86_64 x86_64 x86_64 GNU/Linux
+"""]]
+
+### Please provide any additional information below.
+
+Sorry but I don't know what else could help you.
+
+[[!format sh """
+# If you can, paste a complete transcript of the problem occurring here.
+# If the problem is with the git-annex assistant, paste in .git/annex/daemon.log
+
+
+# End of transcript or log.
+"""]]
+
+> fixed in git and will update cabal soon [[done]] --[[Joey]]
diff --git a/doc/bugs/Repository_in_manual_mode_does_not_hold_files.mdwn b/doc/bugs/Repository_in_manual_mode_does_not_hold_files.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/bugs/Repository_in_manual_mode_does_not_hold_files.mdwn
@@ -0,0 +1,297 @@
+### Please describe the problem.
+
+I have two repositories in my local network which are locally paired and synced using git-annex assistant (setup using webapp, both in direct mode). The one (master) has all files and is in mode "full backup". The second one (slave) is in "manual" mode and should therefore only contain the file content it already has. But it also should not loose any content it has until I explicitely drop it but this is exactly what happens! Files are getting dropped (I think it happens during git-annex startup of the slave repository, but I am not sure).
+
+### What steps will reproduce the problem?
+
+1. Setup two repositories using git-annex webapp in local network
+2. Set one to "full backup" mode, the second to "manual" mode
+3. Add files to the master repository
+4. Pair both repositories over webapp
+5. Call git-annex get folderA on slave system to transfer some file contents to it.
+
+=> After some time the file contents from folderA seem to disappear on slave system.
+
+### What version of git-annex are you using? On what operating system?
+
+* Master (full backup) repository is running on Ubuntu Server 12.04 and git-annex 5.20140117.1 from PPA. Git version 1.7.9.5
+* Slave (manual) repository is running on Gentoo Linux with 5.20140116, created from own ebuild. Git version 1.8.4.5
+
+### Please provide any additional information below.
+
+[[!format sh """
+[2014-01-29 09:14:15 CET] main: starting assistant version 5.20140116
+[2014-01-29 09:19:33 CET] TransferScanner: Syncing with Eifel.fritz.box__mnt_raid_Media 
+Already up-to-date.
+
+(scanning...) [2014-01-29 09:19:34 CET] Watcher: Performing startup scan
+Already up-to-date.
+Already up-to-date.
+[2014-01-29 09:35:31 CET] Committer: Committing changes to git
+[2014-01-29 09:35:31 CET] Pusher: Syncing with Eifel.fritz.box__mnt_raid_Media 
+[2014-01-29 09:40:37 CET] Committer: Committing changes to git
+[2014-01-29 09:44:15 CET] Committer: Committing changes to git
+Von ssh://git-annex-Eifel.fritz.box-fabian_.2Fmnt.2Fraid.2FMedia/mnt/raid/Media
+   390c764..7775ce1  annex/direct/master -> Eifel.fritz.box__mnt_raid_Media/annex/direct/master
+   eca59d1..59db343  git-annex  -> Eifel.fritz.box__mnt_raid_Media/git-annex
+ + a1f3176...7775ce1 synced/master -> Eifel.fritz.box__mnt_raid_Media/synced/master  (Aktualisierung erzwungen)
+Already up-to-date.
+error: Ref refs/heads/synced/master is at a1f3176ff3821cdd9aa74bfa310dfdccb8452247 but expected 7775ce196da4561367ee231ce116fe5849827c51
+remote: error: failed to lock refs/heads/synced/master        
+To ssh://fabian@git-annex-Eifel.fritz.box-fabian_.2Fmnt.2Fraid.2FMedia/mnt/raid/Media/
+   98219eb..3d2b713  git-annex -> synced/git-annex
+ ! [remote rejected] annex/direct/master -> synced/master (failed to lock)
+error: Fehler beim Versenden einiger Referenzen nach 'ssh://fabian@git-annex-Eifel.fritz.box-fabian_.2Fmnt.2Fraid.2FMedia/mnt/raid/Media/'
+Von ssh://git-annex-Eifel.fritz.box-fabian_.2Fmnt.2Fraid.2FMedia/mnt/raid/Media
+   7775ce1..a1f3176  annex/direct/master -> Eifel.fritz.box__mnt_raid_Media/annex/direct/master
+   59db343..6e39b5b  git-annex  -> Eifel.fritz.box__mnt_raid_Media/git-annex
+   7775ce1..a1f3176  synced/master -> Eifel.fritz.box__mnt_raid_Media/synced/master
+Already up-to-date.
+[2014-01-29 09:51:41 CET] Committer: Committing changes to git
+error: Ref refs/heads/synced/git-annex is at 3d2b7131d39c78fc56f67e29617e72b177807449 but expected 98219eb545d5dc51da9984ede4b4c5c41ec188d0
+remote: error: failed to lock refs/heads/synced/git-annex        
+To ssh://fabian@git-annex-Eifel.fritz.box-fabian_.2Fmnt.2Fraid.2FMedia/mnt/raid/Media/
+ ! [remote rejected] git-annex -> synced/git-annex (failed to lock)
+error: Fehler beim Versenden einiger Referenzen nach 'ssh://fabian@git-annex-Eifel.fritz.box-fabian_.2Fmnt.2Fraid.2FMedia/mnt/raid/Media/'
+To ssh://fabian@git-annex-Eifel.fritz.box-fabian_.2Fmnt.2Fraid.2FMedia/mnt/raid/Media/
+   3d2b713..5bd0d7e  git-annex -> synced/git-annex
+[2014-01-29 09:51:51 CET] Pusher: Syncing with Eifel.fritz.box__mnt_raid_Media 
+Everything up-to-date
+Everything up-to-date
+[2014-01-29 09:53:01 CET] Committer: Committing changes to git
+[2014-01-29 09:53:01 CET] Pusher: Syncing with Eifel.fritz.box__mnt_raid_Media 
+Everything up-to-date
+
+
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(merging synced/git-annex into git-annex...)
+(Recording state in git...)
+(merging synced/git-annex into git-annex...)
+(Recording state in git...)
+
+
+(Recording state in git...)
+(Recording state in git...)
+(started...) [2014-01-29 09:53:48 CET] Committer: Committing changes to git
+[2014-01-29 09:53:48 CET] Pusher: Syncing with Eifel.fritz.box__mnt_raid_Media 
+Everything up-to-date
+[2014-01-29 10:19:33 CET] NetWatcherFallback: Syncing with Eifel.fritz.box__mnt_raid_Media 
+Von ssh://git-annex-Eifel.fritz.box-fabian_.2Fmnt.2Fraid.2FMedia/mnt/raid/Media
+   6e39b5b..5bd0d7e  git-annex  -> Eifel.fritz.box__mnt_raid_Media/git-annex
+Everything up-to-date
+[2014-01-29 10:39:49 CET] Committer: Committing changes to git
+[2014-01-29 10:39:49 CET] Pusher: Syncing with Eifel.fritz.box__mnt_raid_Media 
+[2014-01-29 10:39:50 CET] Committer: Committing changes to git
+[2014-01-29 10:39:51 CET] Committer: Committing changes to git
+[2014-01-29 10:39:53 CET] Committer: Committing changes to git
+[2014-01-29 10:39:54 CET] Committer: Committing changes to git
+[2014-01-29 10:39:55 CET] Committer: Committing changes to git
+[2014-01-29 10:39:56 CET] Committer: Committing changes to git
+[2014-01-29 10:39:57 CET] Committer: Committing changes to git
+[2014-01-29 10:39:59 CET] Committer: Committing changes to git
+[2014-01-29 10:40:00 CET] Committer: Committing changes to git
+[2014-01-29 10:40:01 CET] Committer: Committing changes to git
+[2014-01-29 10:40:02 CET] Committer: Committing changes to git
+[2014-01-29 10:40:03 CET] Committer: Committing changes to git
+[2014-01-29 10:40:05 CET] Committer: Committing changes to git
+[2014-01-29 10:40:07 CET] Committer: Adding AlbumArtSmall.jpg Folder.jpg
+
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+add Audio/Musik/2raumwohnung/2002 - In Wirklich/AlbumArtSmall.jpg ok
+add Audio/Musik/2raumwohnung/2002 - In Wirklich/Folder.jpg [2014-01-29 10:40:07 CET] Committer: Committing changes to git
+[2014-01-29 10:40:08 CET] Committer: Committing changes to git
+[2014-01-29 10:40:09 CET] Committer: Committing changes to git
+[2014-01-29 10:40:11 CET] Committer: Committing changes to git
+[2014-01-29 10:40:12 CET] Committer: Committing changes to git
+[2014-01-29 10:40:13 CET] Committer: Committing changes to git
+[2014-01-29 10:40:14 CET] Committer: Committing changes to git
+[2014-01-29 10:40:16 CET] Committer: Committing changes to git
+[2014-01-29 10:40:17 CET] Committer: Committing changes to git
+[2014-01-29 10:40:18 CET] Committer: Committing changes to git
+[2014-01-29 10:40:19 CET] Committer: Committing changes to git
+[2014-01-29 10:40:20 CET] Committer: Committing changes to git
+[2014-01-29 10:40:21 CET] Committer: Committing changes to git
+[2014-01-29 10:40:23 CET] Committer: Committing changes to git
+[2014-01-29 10:40:24 CET] Committer: Committing changes to git
+[2014-01-29 10:40:26 CET] Committer: Committing changes to git
+[2014-01-29 10:40:27 CET] Committer: Committing changes to git
+[2014-01-29 10:40:28 CET] Committer: Committing changes to git
+[2014-01-29 10:40:29 CET] Committer: Committing changes to git
+[2014-01-29 10:40:30 CET] Committer: Committing changes to git
+[2014-01-29 10:40:31 CET] Committer: Committing changes to git
+[2014-01-29 10:40:32 CET] Committer: Committing changes to git
+[2014-01-29 10:40:34 CET] Committer: Committing changes to git
+[2014-01-29 10:40:35 CET] Committer: Committing changes to git
+[2014-01-29 10:40:36 CET] Committer: Committing changes to git
+[2014-01-29 10:40:37 CET] Committer: Committing changes to git
+[2014-01-29 10:40:39 CET] Committer: Committing changes to git
+[2014-01-29 10:40:40 CET] Committer: Committing changes to git
+[2014-01-29 10:40:41 CET] Committer: Committing changes to git
+[2014-01-29 10:40:42 CET] Committer: Committing changes to git
+[2014-01-29 10:40:44 CET] Committer: Committing changes to git
+[2014-01-29 10:40:45 CET] Committer: Committing changes to git
+[2014-01-29 10:40:46 CET] Committer: Committing changes to git
+[2014-01-29 10:40:47 CET] Committer: Committing changes to git
+[2014-01-29 10:40:48 CET] Committer: Committing changes to git
+[2014-01-29 10:40:50 CET] Committer: Committing changes to git
+[2014-01-29 10:40:51 CET] Committer: Committing changes to git
+[2014-01-29 10:40:52 CET] Committer: Committing changes to git
+[2014-01-29 10:40:53 CET] Committer: Committing changes to git
+[2014-01-29 10:40:54 CET] Committer: Committing changes to git
+[2014-01-29 10:40:55 CET] Committer: Committing changes to git
+[2014-01-29 10:40:57 CET] Committer: Committing changes to git
+[2014-01-29 10:40:58 CET] Committer: Committing changes to git
+[2014-01-29 10:40:59 CET] Committer: Committing changes to git
+[2014-01-29 10:41:00 CET] Committer: Committing changes to git
+[2014-01-29 10:41:02 CET] Committer: Committing changes to git
+[2014-01-29 10:41:03 CET] Committer: Committing changes to git
+[2014-01-29 10:41:04 CET] Committer: Committing changes to git
+[2014-01-29 10:41:05 CET] Committer: Committing changes to git
+[2014-01-29 10:41:06 CET] Committer: Committing changes to git
+[2014-01-29 10:41:07 CET] Committer: Committing changes to git
+[2014-01-29 10:41:09 CET] Committer: Committing changes to git
+[2014-01-29 10:41:10 CET] Committer: Committing changes to git
+[2014-01-29 10:41:11 CET] Committer: Committing changes to git
+[2014-01-29 10:41:12 CET] Committer: Committing changes to git
+[2014-01-29 10:41:23 CET] Committer: Committing changes to git
+[2014-01-29 10:41:24 CET] Committer: Committing changes to git
+[2014-01-29 10:41:29 CET] Committer: Committing changes to git
+[2014-01-29 10:41:30 CET] Committer: Committing changes to git
+[2014-01-29 10:41:32 CET] Committer: Committing changes to git
+[2014-01-29 10:41:33 CET] Committer: Committing changes to git
+[2014-01-29 10:41:34 CET] Committer: Committing changes to git
+[2014-01-29 10:41:35 CET] Committer: Committing changes to git
+[2014-01-29 10:41:36 CET] Committer: Committing changes to git
+[2014-01-29 10:41:38 CET] Committer: Adding AlbumArt_..Small.jpg Folder.jpg
+ok
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+(Recording state in git...)
+add Audio/Musik/Alanis Morissette/1995 - Jagged Little Pill/AlbumArt_{79951A99-BD71-4029-80F6-C3705D930871}_Small.jpg ok
+add Audio/Musik/Alanis Morissette/1995 - Jagged Little Pill/Folder.jpg [2014-01-29 10:41:38 CET] Committer: Committing changes to git
+[2014-01-29 10:41:39 CET] Committer: Committing changes to git
+[2014-01-29 10:41:41 CET] Committer: Committing changes to git
+[2014-01-29 10:41:42 CET] Committer: Committing changes to git
+[2014-01-29 10:41:44 CET] Committer: Committing changes to git
+[2014-01-29 10:41:45 CET] Committer: Committing changes to git
+[2014-01-29 10:41:46 CET] Committer: Committing changes to git
+[2014-01-29 10:41:47 CET] Committer: Committing changes to git
+[2014-01-29 10:41:48 CET] Committer: Committing changes to git
+[2014-01-29 10:41:49 CET] Committer: Committing changes to git
+[2014-01-29 10:41:51 CET] Committer: Committing changes to git
+[2014-01-29 10:41:52 CET] Committer: Committing changes to git
+[2014-01-29 10:41:54 CET] Committer: Committing changes to git
+[2014-01-29 10:41:55 CET] Committer: Committing changes to git
+[2014-01-29 10:41:56 CET] Committer: Committing changes to git
+[2014-01-29 10:41:58 CET] Committer: Committing changes to git
+[2014-01-29 10:41:59 CET] Committer: Committing changes to git
+[2014-01-29 10:42:00 CET] Committer: Committing changes to git
+[2014-01-29 10:42:01 CET] Committer: Committing changes to git
+[2014-01-29 10:42:02 CET] Committer: Committing changes to git
+[2014-01-29 10:42:04 CET] Committer: Committing changes to git
+[2014-01-29 10:42:05 CET] Committer: Committing changes to git
+[2014-01-29 10:42:06 CET] Committer: Committing changes to git
+[2014-01-29 10:42:07 CET] Committer: Committing changes to git
+[2014-01-29 10:42:08 CET] Committer: Committing changes to git
+[2014-01-29 10:42:10 CET] Committer: Committing changes to git
+[2014-01-29 10:42:11 CET] Committer: Committing changes to git
+[2014-01-29 10:42:12 CET] Committer: Committing changes to git
+[2014-01-29 10:42:13 CET] Committer: Committing changes to git
+[2014-01-29 10:42:14 CET] Committer: Committing changes to git
+[2014-01-29 10:42:15 CET] Committer: Committing changes to git
+[2014-01-29 10:42:17 CET] Committer: Committing changes to git
+[2014-01-29 10:42:18 CET] Committer: Committing changes to git
+[2014-01-29 10:42:19 CET] Committer: Committing changes to git
+[2014-01-29 10:42:20 CET] Committer: Committing changes to git
+[2014-01-29 10:42:21 CET] Committer: Committing changes to git
+[2014-01-29 10:42:23 CET] Committer: Committing changes to git
+[2014-01-29 10:42:24 CET] Committer: Committing changes to git
+[2014-01-29 10:42:25 CET] Committer: Committing changes to git
+[2014-01-29 10:42:26 CET] Committer: Committing changes to git
+[2014-01-29 10:42:27 CET] Committer: Committing changes to git
+[2014-01-29 10:42:28 CET] Committer: Committing changes to git
+To ssh://fabian@git-annex-Eifel.fritz.box-fabian_.2Fmnt.2Fraid.2FMedia/mnt/raid/Media/
+   5bd0d7e..1eda3be  git-annex -> synced/git-annex
+[2014-01-29 10:43:43 CET] Pusher: Syncing with Eifel.fritz.box__mnt_raid_Media 
+To ssh://fabian@git-annex-Eifel.fritz.box-fabian_.2Fmnt.2Fraid.2FMedia/mnt/raid/Media/
+   1eda3be..4c70ad2  git-annex -> synced/git-annex
+"""]]
diff --git a/doc/bugs/S3_memory_leaks/comment_2_320a8e3bb7b207d1aff8926b9247f5ba._comment b/doc/bugs/S3_memory_leaks/comment_2_320a8e3bb7b207d1aff8926b9247f5ba._comment
new file mode 100644
--- /dev/null
+++ b/doc/bugs/S3_memory_leaks/comment_2_320a8e3bb7b207d1aff8926b9247f5ba._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="http://schnouki.net/"
+ nickname="Schnouki"
+ subject="comment 2"
+ date="2014-01-29T01:19:27Z"
+ content="""
+Any news about this?
+"""]]
diff --git a/doc/bugs/assistant_on_windows_adding_remote_containing_linux_paths.mdwn b/doc/bugs/assistant_on_windows_adding_remote_containing_linux_paths.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/bugs/assistant_on_windows_adding_remote_containing_linux_paths.mdwn
@@ -0,0 +1,14 @@
+### Please describe the problem.
+
+Internal Server Error
+
+internal error, /home/michele/assistannex is not absolute
+
+### What steps will reproduce the problem?
+
+create a transfer repository on a usb drive (from windows) merge it with a repository on linux, try to merge it on another target windows machine
+
+### What version of git-annex are you using? On what operating system?
+
+git-annex version 5.20140128-g29aea74
+
diff --git a/doc/bugs/sync_--content_tries_to_copy_content_to_metadata_only_repos.mdwn b/doc/bugs/sync_--content_tries_to_copy_content_to_metadata_only_repos.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/bugs/sync_--content_tries_to_copy_content_to_metadata_only_repos.mdwn
@@ -0,0 +1,27 @@
+### Please describe the problem.
+
+
+### What steps will reproduce the problem?
+
+git annex sync --content
+
+
+### What version of git-annex are you using? On what operating system?
+
+Mac OSX Maverics
+
+git-annex version: 5.20140127
+build flags: Assistant Webapp Pairing Testsuite S3 WebDAV FsEvents XMPP DNS Feeds Quvi TDFA CryptoHash
+key/value backends: SHA256E SHA1E SHA512E SHA224E SHA384E SKEIN256E SKEIN512E SHA256 SHA1 SHA512 SHA224 SHA384 SKEIN256 SKEIN512 WORM URL
+
+### Please provide any additional information below.
+
+[[!format sh """
+copy Books/Paperless - Agile Bits Edition/Paperless Video/5-4 PDFpen for iPad.m4v (to origin...)
+FATAL: suspicious characters loitering about 'git-annex-shell 'recvkey' '/~/users/akraut/annex-home' 'SHA256E-s98445427--5fb5fd6e082eec4a805261764ef982aa8f12d76e07e86a6abb05e7675762ac49.m4v' '--' 'remoteuuid=03ac7aa9-d14c-4b60-adae-02e4a5ec0fa8' 'direct=' 'associatedfile=Books/Paperless - Agile Bits Edition/Paperless Video/5-4 PDFpen for iPad.m4v' '--' dummy rsync --server -v --inplace .'
+rsync: connection unexpectedly closed (0 bytes received so far) [sender]
+rsync error: the --max-delete limit stopped deletions (code 25) at /SourceCache/rsync/rsync-42/rsync/io.c(452) [sender=2.6.9]
+
+  rsync failed -- run git annex again to resume file transfer
+failed
+"""]]
diff --git a/doc/bugs/tweaks_to_directory_special_remote_doco.mdwn b/doc/bugs/tweaks_to_directory_special_remote_doco.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/bugs/tweaks_to_directory_special_remote_doco.mdwn
@@ -0,0 +1,74 @@
+### Please describe the problem.
+
+I found the discussion in [directory](/special_remotes/directory) quite confusing until I looked at it the right way. Some tweaking of the documentation might help.
+
+### What steps will reproduce the problem?
+
+Possible method - get a newbie to read the page.
+
+### What version of git-annex are you using? On what operating system?
+
+n/a
+
+### Please provide any additional information below.
+
+Below is an untested patch that I think would make the documentation more helpful to me on a first reading.
+
+      Tweaks to doc/special_remotes/directory.mdwn
+      
+        * document the 'directory' option (!)
+        * try to make it clearer what is different about this remote,
+          including giving an example of how the directory structure looks.
+        * grammar fix in opening paragraph
+      
+      ---
+       doc/special_remotes/directory.mdwn | 16 +++++++++++++++-
+       1 file changed, 15 insertions(+), 1 deletion(-)
+      
+      diff --git a/doc/special_remotes/directory.mdwn b/doc/special_remotes/directory.mdwn
+      index 4d72e8b..7f076b3 100644
+      --- a/doc/special_remotes/directory.mdwn
+      +++ b/doc/special_remotes/directory.mdwn
+      @@ -1,10 +1,12 @@
+       This special remote type stores file contents in directory.
+       
+       One use case for this would be if you have a removable drive that
+      -you want to use it to sneakernet files between systems (possibly with
+      +you want to use to sneakernet files between systems (possibly with
+       \[[encrypted|encryption]] contents). Just set up both systems to use
+       the drive's mountpoint as a directory remote.
+       
+      +Note that directory remotes have a special directory structure
+      +(by design, the same as the \[[rsync|rsync]] remote).
+       If you just want two copies of your repository with the files "visible"
+       in the tree in both, the directory special remote is not what you want.
+       Instead, you should use a regular `git clone` of your git-annex repository.
+      @@ -14,6 +16,8 @@ Instead, you should use a regular `git clone` of your git-annex repository.
+       These parameters can be passed to `git annex initremote` to configure the
+       remote:
+       
+      +* `directory` - The path to directory in which the remote resides
+      +
+       * `encryption` - One of "none", "hybrid", "shared", or "pubkey".
+         See \[[encryption]].
+       
+      @@ -31,3 +35,13 @@ Setup example:
+       
+        # git annex initremote usbdrive type=directory directory=/media/usbdrive/ encryption=none
+        # git annex describe usbdrive "usb drive on /media/usbdrive/"
+      +
+      +Usage example:
+      + # git annex copy mycoolfile.mp4 --to usbdrive
+      + # ls -aF /media/usbdrive
+      +        ./  ../  42b/  .git/  tmp/
+      + # git annex whereis mycoolfile.mp4
+      + whereis mycoolfile.mp4 (2 copies)
+      +         320053d5-892f-46d2-89f0-d6e9d09e6398 -- here
+      +         6747a48b-fad2-41a7-9033-8d8daa35c5f8 -- usbdrive
+      + ok
+      -- 
+      1.8.5.2
+
+
+
+# End of transcript or log.
diff --git a/doc/devblog/day_105__locking.mdwn b/doc/devblog/day_105__locking.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/devblog/day_105__locking.mdwn
@@ -0,0 +1,30 @@
+With yesterday's release, I'm pretty much done with the month's work. Since
+there was no particular goal this month, it's been a grab bag of features
+and bugfixes. Quite a lot of them in this last release.
+
+I'll be away the next couple of days.. But got a start today on the next
+part of the roadmap, which is planned to be all about Windows and Android
+porting. Today, it was all about lock files, mostly on Windows.
+
+Lock files on Windows are horrific. I especially like that programs that
+want to open a file, for any reason, are encouraged in the official
+documentation to retry repeatedly if it fails, because some other
+random program, like a virus checker, might have opened the file first.
+
+Turns out Windows does support a shared file read mode. This was
+just barely enough for me to implement both shared and exclusive
+file locking a-la-flock.
+
+Couldn't avoid a busy wait in a few places that block on a lock.
+Luckily, these are few, and the chances the lock will be taken for a long
+time is small. (I did think about trying to watch the file for close events
+and detect when the lock was released that way, but it seemed much too
+complicated and hard to avoid races.)
+
+Also, Windows only seems to support mandatory locks, while all locking in
+git-annex needs to be advisory locks. Ie, git-annex's locking shouldn't
+prevent a program from opening an annexed file! To work around that,
+I am using dedicated lock files on Windows.
+
+Also switched direct mode's annexed object locking to use dedicated lock
+files. AFAICS, this was pretty well broken in direct mode before.
diff --git a/doc/forum/howto_to_link_to_existing_direct_mode_git-annexes.mdwn b/doc/forum/howto_to_link_to_existing_direct_mode_git-annexes.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/forum/howto_to_link_to_existing_direct_mode_git-annexes.mdwn
@@ -0,0 +1,5 @@
+I have created two git-annexes (one on my laptop and one on my work pc) The are using nearly the same files with some files existing only on the pc but not on the laptop as they are too large for the laptop. How do I get the two into sync. I found the manual in the tips section but that will create normal git-annexes with all files linked only. However, I would like to have my exiting file structure unchanged (the way the assistant does it by default)
+
+any ideas?
+
+Gregor
diff --git a/doc/special_remotes/xmpp.mdwn b/doc/special_remotes/xmpp.mdwn
--- a/doc/special_remotes/xmpp.mdwn
+++ b/doc/special_remotes/xmpp.mdwn
@@ -31,7 +31,7 @@
 [[jabber.org|http://jabber.org]]|Working|[[Isode M-Link|http://www.isode.com/products/m-link.html]]
 -|Working|[[Prosody|http://prosody.im/]]|No providers tested.
 -|Working|[[Metronome|http://www.lightwitch.org/]]|No providers tested.
--|[[Failing|http://git-annex.branchable.com/forum/XMPP_authentication_failure/]]|ejabberd|[[Authentication bug|https://support.process-one.net/browse/EJAB-1632]]: Fixed in debian unstable with version 2.1.10-5
+-|[[Failing|http://git-annex.branchable.com/forum/XMPP_authentication_failure/]]|ejabberd|[[Authentication bug|https://support.process-one.net/browse/EJAB-1632]]: Fixed in debian unstable (>= 2.1.10-5) and stable (>=2.1.10-4+deb7u1)
 -|[[Failing|http://git-annex.branchable.com/forum/XMPP_authentication_failure/#comment-4ce5aeabd12ca3016290b3d8255f6ef1]]|jabberd14|No further information
 """]]
 List of providers: [[http://xmpp.net/]]
diff --git a/doc/todo/windows_support.mdwn b/doc/todo/windows_support.mdwn
--- a/doc/todo/windows_support.mdwn
+++ b/doc/todo/windows_support.mdwn
@@ -7,8 +7,6 @@
   support use of DOS style paths, which git-annex uses on Windows). 
   Must use Msysgit.
 * rsync special remotes are known buggy.
-* Bad file locking, it's probably not safe to run more than one git-annex
-  process at the same time on Windows.
 * Ssh connection caching does not work on Windows, so `git annex get`
   has to connect twice to the remote system over ssh per file, which
   is much slower than on systems supporting connection caching.
diff --git a/git-annex.cabal b/git-annex.cabal
--- a/git-annex.cabal
+++ b/git-annex.cabal
@@ -1,5 +1,5 @@
 Name: git-annex
-Version: 5.20140127
+Version: 5.20140129
 Cabal-Version: >= 1.8
 License: GPL-3
 Maintainer: Joey Hess <joey@kitenet.net>
@@ -114,7 +114,7 @@
     CPP-Options: -DWITH_CLIBS
 
   if flag(TestSuite)
-    Build-Depends: tasty (>= 0.7), tasty-hunit, tasty-quickcheck,
+    Build-Depends: tasty (>= 0.7), tasty-hunit, tasty-quickcheck, tasty-rerun,
      optparse-applicative
     CPP-Options: -DWITH_TESTSUITE
 
