diff --git a/Annex/Branch.hs b/Annex/Branch.hs
--- a/Annex/Branch.hs
+++ b/Annex/Branch.hs
@@ -280,8 +280,11 @@
 	st <- getState
 	content <- if journalIgnorable st
 		then pure branchcontent
-		else fromMaybe branchcontent
-			<$> getJournalFileStale (GetPrivate True) file
+		else getJournalFileStale (GetPrivate True) file >>= return . \case
+			NoJournalledContent -> branchcontent
+			JournalledContent journalcontent -> journalcontent
+			PossiblyStaleJournalledContent journalcontent ->
+				branchcontent <> journalcontent
 	Annex.BranchState.setCache file content
 
 {- Like get, but does not merge the branch, so the info returned may not
@@ -296,8 +299,11 @@
 	fastDebug "Annex.Branch" ("read " ++ fromRawFilePath file)
 	go =<< getJournalFileStale getprivate file
   where
-	go (Just journalcontent) = return journalcontent
-	go Nothing = getRef fullname file
+	go NoJournalledContent = getRef fullname file
+	go (JournalledContent journalcontent) = return journalcontent
+	go (PossiblyStaleJournalledContent journalcontent) = do
+		v <- getRef fullname file
+		return (v <> journalcontent)
 
 {- Gets the content of a file as staged in the branch's index. -}
 getStaged :: RawFilePath -> Annex L.ByteString
@@ -813,12 +819,7 @@
 	buf <- liftIO newEmptyMVar
 	let go' reader = go $ liftIO reader >>= \case
 		Just ((v, f), content) -> do
-			-- Check the journal if it did not get
-			-- committed to the branch
-			content' <- if journalIgnorable st
-				then pure content
-				else maybe content Just 
-					<$> getJournalFileStale (GetPrivate True) f
+			content' <- checkjournal st f content
 			return (Just (v, f, content'))
 		Nothing
 			| journalIgnorable st -> return Nothing
@@ -834,16 +835,36 @@
 	catObjectStreamLsTree l (select' . getTopFilePath . Git.LsTree.file) g go'
 		`finally` liftIO (void cleanup)
   where
-	getnext [] = Nothing
-	getnext (f:fs) = case select f of
-		Nothing -> getnext fs
-		Just v -> Just (v, f, fs)
-					
+	-- Check the journal, in case it did not get committed to the branch
+	checkjournal st f branchcontent
+		| journalIgnorable st = return branchcontent
+		| otherwise = getJournalFileStale (GetPrivate True) f >>= return . \case
+			NoJournalledContent -> branchcontent
+			JournalledContent journalledcontent ->
+				Just journalledcontent
+			PossiblyStaleJournalledContent journalledcontent ->
+				Just (fromMaybe mempty branchcontent <> journalledcontent)
+				
 	drain buf fs = case getnext fs of
 		Just (v, f, fs') -> do
 			liftIO $ putMVar buf fs'
-			content <- getJournalFileStale (GetPrivate True) f
+			content <- getJournalFileStale (GetPrivate True) f >>= \case
+				NoJournalledContent -> return Nothing
+				JournalledContent journalledcontent ->
+					return (Just journalledcontent)
+				PossiblyStaleJournalledContent journalledcontent -> do
+					-- This is expensive, but happens
+					-- only when there is a private
+					-- journal file.
+					content <- getRef fullname f
+					return (Just (content <> journalledcontent))
 			return (Just (v, f, content))
 		Nothing -> do
 			liftIO $ putMVar buf []
 			return Nothing
+	
+	getnext [] = Nothing
+	getnext (f:fs) = case select f of
+		Nothing -> getnext fs
+		Just v -> Just (v, f, fs)
+
diff --git a/Annex/Content.hs b/Annex/Content.hs
--- a/Annex/Content.hs
+++ b/Annex/Content.hs
@@ -342,10 +342,13 @@
 	storeobject dest = ifM (liftIO $ R.doesPathExist dest)
 		( alreadyhave
 		, adjustedBranchRefresh af $ modifyContent dest $ do
-			freezeContent src
 			liftIO $ moveFile
 				(fromRawFilePath src)
 				(fromRawFilePath dest)
+			-- Freeze the object file now that it is in place.
+			-- Waiting until now to freeze it allows for freeze
+			-- hooks that prevent moving the file.
+			freezeContent dest
 			g <- Annex.gitRepo 
 			fs <- map (`fromTopFilePath` g)
 				<$> Database.Keys.getAssociatedFiles key
diff --git a/Annex/Content/PointerFile.hs b/Annex/Content/PointerFile.hs
--- a/Annex/Content/PointerFile.hs
+++ b/Annex/Content/PointerFile.hs
@@ -19,7 +19,7 @@
 import qualified Utility.RawFilePath as R
 #if ! defined(mingw32_HOST_OS)
 import Utility.Touch
-import System.Posix.Files (modificationTimeHiRes)
+import qualified System.Posix.Files as Posix
 #endif
 
 {- Populates a pointer file with the content of a key. 
@@ -66,7 +66,7 @@
 		-- by git in some cases.
 		liftIO $ maybe noop
 			(\t -> touch tmp' t False)
-			(fmap modificationTimeHiRes st)
+			(fmap Posix.modificationTimeHiRes st)
 #endif
 		withTSDelta (liftIO . genInodeCache (toRawFilePath tmp))
 	maybe noop (restagePointerFile (Restage True) file) ic
diff --git a/Annex/Journal.hs b/Annex/Journal.hs
--- a/Annex/Journal.hs
+++ b/Annex/Journal.hs
@@ -89,8 +89,20 @@
 		withFile tmpfile WriteMode $ \h -> writeJournalHandle h content
 		moveFile tmpfile (fromRawFilePath (jd P.</> jfile))
 
+data JournalledContent
+	= NoJournalledContent
+	| JournalledContent L.ByteString
+	| PossiblyStaleJournalledContent L.ByteString
+	-- ^ This is used when the journalled content may have been 
+	-- supersceded by content in the git-annex branch. The returned
+	-- content should be combined with content from the git-annex branch.
+	-- This is particularly the case when a file is in the private
+	-- journal, which does not get written to the git-annex branch,
+	-- and so the git-annex branch can contain changes to non-private
+	-- information that were made after that journal file was written.
+
 {- Gets any journalled content for a file in the branch. -}
-getJournalFile :: JournalLocked -> GetPrivate -> RawFilePath -> Annex (Maybe L.ByteString)
+getJournalFile :: JournalLocked -> GetPrivate -> RawFilePath -> Annex JournalledContent
 getJournalFile _jl = getJournalFileStale
 
 data GetPrivate = GetPrivate Bool
@@ -106,7 +118,7 @@
  - concurrency or other issues with a lazy read, and the minor loss of
  - laziness doesn't matter much, as the files are not very large.
  -}
-getJournalFileStale :: GetPrivate -> RawFilePath -> Annex (Maybe L.ByteString)
+getJournalFileStale :: GetPrivate -> RawFilePath -> Annex JournalledContent
 getJournalFileStale (GetPrivate getprivate) file = do
 	-- Optimisation to avoid a second MVar access.
 	st <- Annex.getState id
@@ -115,11 +127,19 @@
 		if getprivate && privateUUIDsKnown' st
 		then do
 			x <- getfrom (gitAnnexJournalDir g)
-			y <- getfrom (gitAnnexPrivateJournalDir g)
-			-- This concacenation is the same as happens in a
-			-- merge of two git-annex branches.
-			return (x <> y)
-		else getfrom (gitAnnexJournalDir g)
+			getfrom (gitAnnexPrivateJournalDir g) >>= \case
+				Nothing -> return $ case x of
+					Nothing -> NoJournalledContent
+					Just b -> JournalledContent b
+				Just y -> return $ PossiblyStaleJournalledContent $ case x of
+					Nothing -> y
+					-- This concacenation is the same as
+					-- happens in a merge of two
+					-- git-annex branches.
+					Just x' -> x' <> y
+		else getfrom (gitAnnexJournalDir g) >>= return . \case
+			Nothing -> NoJournalledContent
+			Just b -> JournalledContent b
   where
 	jfile = journalFile file
 	getfrom d = catchMaybeIO $
diff --git a/Annex/Link.hs b/Annex/Link.hs
--- a/Annex/Link.hs
+++ b/Annex/Link.hs
@@ -203,7 +203,12 @@
 	-- updated index file.
 	runner :: Git.Queue.InternalActionRunner Annex
 	runner = Git.Queue.InternalActionRunner "restagePointerFile" $ \r l -> do
-		liftIO . Database.Keys.Handle.flushDbQueue
+		-- Flush any queued changes to the keys database, so they
+		-- are visible to child processes.
+		-- The database is closed because that may improve behavior
+		-- when run in Windows's WSL1, which has issues with
+		-- multiple writers to SQL databases.
+		liftIO . Database.Keys.Handle.closeDbHandle
 			=<< Annex.getRead Annex.keysdbhandle
 		realindex <- liftIO $ Git.Index.currentIndexFile r
 		let lock = fromRawFilePath (Git.Index.indexFileLock realindex)
diff --git a/Annex/Transfer.hs b/Annex/Transfer.hs
--- a/Annex/Transfer.hs
+++ b/Annex/Transfer.hs
@@ -132,7 +132,7 @@
 		(lck, inprogress) <- prep tfile createtfile mode
 		if inprogress && not ignorelock
 			then do
-				showNote "transfer already in progress, or unable to take transfer lock"
+				warning "transfer already in progress, or unable to take transfer lock"
 				return observeFailure
 			else do
 				v <- retry 0 info metervar $
@@ -151,6 +151,11 @@
 		createAnnexDirectory $ P.takeDirectory lck
 		tryLockExclusive (Just mode) lck >>= \case
 			Nothing -> return (Nothing, True)
+			-- Since the lock file is removed in cleanup,
+			-- there's a race where different processes
+			-- may have a deleted and a new version of the same
+			-- lock file open. checkSaneLock guards against
+			-- that.
 			Just lockhandle -> ifM (checkSaneLock lck lockhandle)
 				( do
 					createtfile
diff --git a/Assistant/WebApp/Configurators/IA.hs b/Assistant/WebApp/Configurators/IA.hs
--- a/Assistant/WebApp/Configurators/IA.hs
+++ b/Assistant/WebApp/Configurators/IA.hs
@@ -180,11 +180,11 @@
 getRepoInfo :: RemoteConfig -> Widget
 getRepoInfo c = do
 	uo <- liftAnnex Url.getUrlOptions
-	exists <- liftAnnex $ catchDefaultIO False $ Url.exists url uo
+	urlexists <- liftAnnex $ catchDefaultIO False $ Url.exists url uo
 	[whamlet|
 <a href="#{url}">
   Internet Archive item
-$if (not exists)
+$if (not urlexists)
   <p>
     The page will only be available once some files #
     have been uploaded, and the Internet Archive has processed them.
diff --git a/CHANGELOG b/CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,37 @@
+git-annex (8.20211028) upstream; urgency=medium
+
+  * Removed support for accessing git remotes that use versions of
+    git-annex older than 6.20180312.
+  * git-annex-shell: Removed several commands that were only needed to
+    support git-annex versions older than 6.20180312.
+  * Negotiate P2P protocol version with tor remotes, allowing
+    use of protocol version 1. This negotiation is not supported
+    by versions of git-annex older than 6.20180312.
+  * Fix bug that caused stale git-annex branch information to read
+    when annex.private or remote.name.annex-private is set.
+  * git-annex get when run as the first git-annex command in a new repo
+    did not populate all unlocked files.
+    (Reversion in version 8.20210621)
+  * Fix using lookupkey and info inside a subdirectory of the git repository.
+    (Reversion in version 8.20211011)
+  * Avoid some sqlite crashes on Windows SubSystem for Linux (WSL).
+  * Call annex.freezecontent-command on the annex object file
+    after it has been moved into place in annex/objects/. This allows
+    the hook to freeze the file in ways that prevent moving it, such as
+    removing the Windows delete permission.
+    Thanks, Reiko Asakura.
+  * addurl: Support adding the same url to multiple files at the same
+    time when using -J with --batch --with-files.
+  * When retrieval from a chunked remote fails, display the error that
+    occurred when downloading the chunk, rather than the error that
+    occurred when trying to download the unchunked content, which is less
+    likely to actually be stored in the remote.
+  * Avoid crashing tilde expansion on user who does not exist.
+  * test: Put gpg temp home directory in system temp directory,
+    not filesystem being tested.
+
+ -- Joey Hess <id@joeyh.name>  Thu, 28 Oct 2021 11:58:29 -0400
+
 git-annex (8.20211011) upstream; urgency=medium
 
   * Added annex.bwlimit and remote.name.annex-bwlimit config to limit
diff --git a/CmdLine/GitAnnexShell.hs b/CmdLine/GitAnnexShell.hs
--- a/CmdLine/GitAnnexShell.hs
+++ b/CmdLine/GitAnnexShell.hs
@@ -1,6 +1,6 @@
 {- git-annex-shell main program
  -
- - Copyright 2010-2018 Joey Hess <id@joeyh.name>
+ - Copyright 2010-2021 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -20,16 +20,13 @@
 import P2P.Protocol (ServerMode(..))
 
 import qualified Command.ConfigList
-import qualified Command.InAnnex
-import qualified Command.LockContent
-import qualified Command.DropKey
-import qualified Command.RecvKey
-import qualified Command.SendKey
-import qualified Command.TransferInfo
-import qualified Command.Commit
 import qualified Command.NotifyChanges
 import qualified Command.GCryptSetup
 import qualified Command.P2PStdIO
+import qualified Command.InAnnex
+import qualified Command.RecvKey
+import qualified Command.SendKey
+import qualified Command.DropKey
 
 import qualified Data.Map as M
 
@@ -42,20 +39,17 @@
   where
 	readonlycmds = map addGlobalOptions
 		[ Command.ConfigList.cmd
-		, gitAnnexShellCheck Command.InAnnex.cmd
-		, gitAnnexShellCheck Command.LockContent.cmd
-		, gitAnnexShellCheck Command.SendKey.cmd
-		, gitAnnexShellCheck Command.TransferInfo.cmd
 		, gitAnnexShellCheck Command.NotifyChanges.cmd
 		-- p2pstdio checks the enviroment variables to
 		-- determine the security policy to use
 		, gitAnnexShellCheck Command.P2PStdIO.cmd
+		, gitAnnexShellCheck Command.InAnnex.cmd
+		, gitAnnexShellCheck Command.SendKey.cmd
 		]
 	appendcmds = readonlycmds ++ map addGlobalOptions
 		[ gitAnnexShellCheck Command.RecvKey.cmd
-		, gitAnnexShellCheck Command.Commit.cmd
 		]
-	allcmds = map addGlobalOptions
+	allcmds = appendcmds ++ map addGlobalOptions
 		[ gitAnnexShellCheck Command.DropKey.cmd
 		, Command.GCryptSetup.cmd
 		]
@@ -67,7 +61,7 @@
 cmdsFor = fromMaybe [] . flip M.lookup cmdsMap
 
 cmdsList :: [Command]
-cmdsList = concat $ M.elems cmdsMap
+cmdsList = nub $ concat $ M.elems cmdsMap
 
 addGlobalOptions :: Command -> Command
 addGlobalOptions c = c { cmdglobaloptions = globalOptions ++ cmdglobaloptions c }
@@ -166,9 +160,6 @@
 checkField :: (String, String) -> Bool
 checkField (field, val)
 	| field == fieldName remoteUUID = fieldCheck remoteUUID val
-	| field == fieldName associatedFile = fieldCheck associatedFile val
-	| field == fieldName unlocked = fieldCheck unlocked val
-	| field == fieldName direct = fieldCheck direct val
 	| field == fieldName autoInit = fieldCheck autoInit val
 	| otherwise = False
 
diff --git a/CmdLine/GitAnnexShell/Fields.hs b/CmdLine/GitAnnexShell/Fields.hs
--- a/CmdLine/GitAnnexShell/Fields.hs
+++ b/CmdLine/GitAnnexShell/Fields.hs
@@ -9,7 +9,6 @@
 
 import Annex.Common
 import qualified Annex
-import Git.FilePath
 
 import Data.Char
 
@@ -26,14 +25,6 @@
 remoteUUID = Field "remoteuuid" $
 	-- does it look like a UUID?
 	all (\c -> isAlphaNum c || c == '-')
-
-associatedFile :: Field
-associatedFile = Field "associatedfile" $ \f ->
-	-- is the file a safe relative filename?
-	not (absoluteGitPath (toRawFilePath f)) && not ("../" `isPrefixOf` f)
-
-direct :: Field
-direct = Field "direct" $ \f -> f == "1"
 
 unlocked :: Field
 unlocked = Field "unlocked" $ \f -> f == "1"
diff --git a/Command/AddUrl.hs b/Command/AddUrl.hs
--- a/Command/AddUrl.hs
+++ b/Command/AddUrl.hs
@@ -372,11 +372,21 @@
  - a filename. It's not displayed then for output consistency, 
  - but is added to the json when available. -}
 startingAddUrl :: SeekInput -> URLString -> AddUrlOptions -> CommandPerform -> CommandStart
-startingAddUrl si url o p = starting "addurl" (ActionItemOther (Just url)) si $ do
+startingAddUrl si url o p = starting "addurl" ai si $ do
 	case fileOption (downloadOptions o) of
 		Nothing -> noop
 		Just file -> maybeShowJSON $ JSONChunk [("file", file)]
 	p
+  where
+	-- Avoid failure when the same url is downloaded concurrently
+	-- to two different files, by using OnlyActionOn with a key
+	-- based on the url. Note that this may not be the actual key
+	-- that is used for the download; later size information may be
+	-- available and get added to it. That's ok, this is only
+	-- used to prevent two threads running concurrently when that would
+	-- likely fail.
+	ai = OnlyActionOn urlkey (ActionItemOther (Just url))
+	urlkey = Backend.URL.fromUrl url Nothing
 
 showDestinationFile :: FilePath -> Annex ()
 showDestinationFile file = do
diff --git a/Command/Commit.hs b/Command/Commit.hs
deleted file mode 100644
--- a/Command/Commit.hs
+++ /dev/null
@@ -1,32 +0,0 @@
-{- git-annex command
- -
- - Copyright 2012 Joey Hess <id@joeyh.name>
- -
- - Licensed under the GNU AGPL version 3 or higher.
- -}
-
-module Command.Commit where
-
-import Command
-import qualified Annex.Branch
-import qualified Git
-import Git.Types
-
-cmd :: Command
-cmd = command "commit" SectionPlumbing 
-	"commits any staged changes to the git-annex branch"
-	paramNothing (withParams seek)
-
-seek :: CmdParams -> CommandSeek
-seek = withNothing (commandAction start)
-
-start :: CommandStart
-start = starting "commit" ai si $ do
-	Annex.Branch.commit =<< Annex.Branch.commitMessage
-	_ <- runhook <=< inRepo $ Git.hookPath "annex-content"
-	next $ return True
-  where
-	runhook (Just hook) = liftIO $ boolSystem hook []
-	runhook Nothing = return True
-	ai = ActionItemOther (Just (fromRef Annex.Branch.name))
-	si = SeekInput []
diff --git a/Command/Fix.hs b/Command/Fix.hs
--- a/Command/Fix.hs
+++ b/Command/Fix.hs
@@ -21,7 +21,7 @@
 
 #if ! defined(mingw32_HOST_OS)
 import Utility.Touch
-import System.Posix.Files
+import qualified System.Posix.Files as Posix
 #endif
 
 cmd :: Command
@@ -94,7 +94,7 @@
 fixSymlink file link = do
 #if ! defined(mingw32_HOST_OS)
 	-- preserve mtime of symlink
-	mtime <- liftIO $ catchMaybeIO $ modificationTimeHiRes
+	mtime <- liftIO $ catchMaybeIO $ Posix.modificationTimeHiRes
 		<$> R.getSymbolicLinkStatus file
 #endif
 	createWorkTreeDirectory (parentDir file)
diff --git a/Command/LockContent.hs b/Command/LockContent.hs
deleted file mode 100644
--- a/Command/LockContent.hs
+++ /dev/null
@@ -1,41 +0,0 @@
-{- git-annex-shell command
- -
- - Copyright 2015 Joey Hess <id@joeyh.name>
- -
- - Licensed under the GNU AGPL version 3 or higher.
- -}
-
-module Command.LockContent where
-
-import Command
-import Annex.Content
-import Remote.Helper.Ssh (contentLockedMarker)
-import Utility.SimpleProtocol
-
-cmd :: Command
-cmd = noCommit $ 
-	command "lockcontent" SectionPlumbing 
-		"locks key's content in the annex, preventing it being dropped"
-		paramKey
-		(withParams seek)
-
-seek :: CmdParams -> CommandSeek
-seek = withWords (commandAction . start)
-
--- First, lock the content, then print out "OK". 
--- Wait for the caller to send a line before dropping the lock.
-start :: [String] -> CommandStart
-start [ks] = do
-	ok <- lockContentShared k (const locksuccess)
-		`catchNonAsync` (const $ return False)
-	liftIO $ if ok
-		then exitSuccess
-		else exitFailure
-  where
-	k = fromMaybe (giveup "bad key") (deserializeKey ks)
-	locksuccess = liftIO $ do
-		putStrLn contentLockedMarker
-		hFlush stdout
-		_ <- getProtocolLine stdin
-		return True
-start _ = giveup "Specify exactly 1 key."
diff --git a/Command/RecvKey.hs b/Command/RecvKey.hs
--- a/Command/RecvKey.hs
+++ b/Command/RecvKey.hs
@@ -15,7 +15,6 @@
 import Types.Transfer
 import Logs.Location
 import Command.SendKey (fieldTransfer)
-import qualified CmdLine.GitAnnexShell.Fields as Fields
 
 cmd :: Command
 cmd = noCommit $ command "recvkey" SectionPlumbing 
@@ -27,14 +26,9 @@
 
 start :: (SeekInput, Key) -> CommandStart
 start (_, key) = fieldTransfer Download key $ \_p -> do
-	-- Always verify content when a repo is sending an unlocked file,
-	-- as the file could change while being transferred.
-	fromunlocked <- (isJust <$> Fields.getField Fields.unlocked)
-		<||> (isJust <$> Fields.getField Fields.direct)
-	let verify = if fromunlocked then AlwaysVerify else DefaultVerify
 	-- This matches the retrievalSecurityPolicy of Remote.Git
 	let rsp = RetrievalAllKeysSecure
-	ifM (getViaTmp rsp verify key (AssociatedFile Nothing) go)
+	ifM (getViaTmp rsp DefaultVerify key (AssociatedFile Nothing) go)
 		( do
 			logStatus key InfoPresent
 			-- forcibly quit after receiving one key,
diff --git a/Command/SendKey.hs b/Command/SendKey.hs
--- a/Command/SendKey.hs
+++ b/Command/SendKey.hs
@@ -46,8 +46,7 @@
 fieldTransfer :: Direction -> Key -> (MeterUpdate -> Annex Bool) -> CommandStart
 fieldTransfer direction key a = do
 	fastDebug "Command.SendKey" "transfer start"
-	afile <- AssociatedFile . (fmap toRawFilePath)
-		<$> Fields.getField Fields.associatedFile
+	let afile = AssociatedFile Nothing
 	ok <- maybe (a $ const noop)
 		-- Using noRetry here because we're the sender.
 		(\u -> runner (Transfer direction (toUUID u) (fromKey id key)) afile Nothing noRetry a)
diff --git a/Command/TransferInfo.hs b/Command/TransferInfo.hs
deleted file mode 100644
--- a/Command/TransferInfo.hs
+++ /dev/null
@@ -1,69 +0,0 @@
-{- git-annex command
- -
- - Copyright 2012 Joey Hess <id@joeyh.name>
- -
- - Licensed under the GNU AGPL version 3 or higher.
- -}
-
-module Command.TransferInfo where
-
-import Command
-import Annex.Content
-import Types.Transfer
-import Logs.Transfer
-import Utility.Metered
-import Utility.SimpleProtocol
-import qualified CmdLine.GitAnnexShell.Fields as Fields
-import qualified Utility.RawFilePath as R
-
-cmd :: Command
-cmd = noCommit $ 
-	command "transferinfo" SectionPlumbing
-		"updates sender on number of bytes of content received"
-		paramKey (withParams seek)
-
-seek :: CmdParams -> CommandSeek
-seek = withWords (commandAction . start)
-
-{- Security:
- - 
- - The transfer info file contains the user-supplied key, but
- - the built-in guards prevent slashes in it from showing up in the filename.
- - It also contains the UUID of the remote. But slashes are also filtered
- - out of that when generating the filename.
- - 
- - Checks that the key being transferred is inAnnex, to prevent
- - malicious spamming of bogus keys. Does not check that a transfer
- - of the key is actually in progress, because this could be started
- - concurrently with sendkey, and win the race.
- -}
-start :: [String] -> CommandStart
-start (k:[]) = do
-	case deserializeKey k of
-		Nothing -> error "bad key"
-		(Just key) -> whenM (inAnnex key) $ do
-			afile <- AssociatedFile . (fmap toRawFilePath)
-				<$> Fields.getField Fields.associatedFile
-			u <- maybe (error "missing remoteuuid") toUUID
-				<$> Fields.getField Fields.remoteUUID
-			let t = Transfer
-				{ transferDirection = Upload
-				, transferUUID = u
-				, transferKeyData = fromKey id key
-				}
-			tinfo <- liftIO $ startTransferInfo afile
-			(update, tfile, createtfile, _) <- mkProgressUpdater t tinfo
-			createtfile
-			liftIO $ mapM_ void
-				[ tryIO $ forever $ do
-					bytes <- readUpdate
-					maybe (error "transferinfo protocol error")
-						(update . toBytesProcessed) bytes
-				, tryIO $ R.removeLink tfile
-				, exitSuccess
-				]
-	stop
-start _ = giveup "wrong number of parameters"
-
-readUpdate :: IO (Maybe Integer)
-readUpdate = maybe Nothing readish <$> getProtocolLine stdin
diff --git a/Database/Benchmark.hs b/Database/Benchmark.hs
--- a/Database/Benchmark.hs
+++ b/Database/Benchmark.hs
@@ -107,7 +107,7 @@
 benchDb tmpdir num = do
 	liftIO $ putStrLn $ "setting up database with " ++ show num ++ " items"
 	initDb db SQL.createTables
-	h <- liftIO $ H.openDbQueue H.MultiWriter db SQL.containedTable
+	h <- liftIO $ H.openDbQueue db SQL.containedTable
 	liftIO $ populateAssociatedFiles h num
 	sz <- liftIO $ getFileSize db
 	liftIO $ putStrLn $ "size of database on disk: " ++ 
diff --git a/Database/ContentIdentifier.hs b/Database/ContentIdentifier.hs
--- a/Database/ContentIdentifier.hs
+++ b/Database/ContentIdentifier.hs
@@ -89,7 +89,7 @@
 		, liftIO $ runSqlite (T.pack (fromRawFilePath db)) $ void $
 			runMigrationSilent migrateContentIdentifier
 		)
-	h <- liftIO $ H.openDbQueue H.SingleWriter db "content_identifiers"
+	h <- liftIO $ H.openDbQueue db "content_identifiers"
 	return $ ContentIdentifierHandle h
 
 closeDb :: ContentIdentifierHandle -> Annex ()
diff --git a/Database/Export.hs b/Database/Export.hs
--- a/Database/Export.hs
+++ b/Database/Export.hs
@@ -104,7 +104,7 @@
 	unlessM (liftIO $ R.doesPathExist db) $ do
 		initDb db $ void $
 			runMigrationSilent migrateExport
-	h <- liftIO $ H.openDbQueue H.SingleWriter db "exported"
+	h <- liftIO $ H.openDbQueue db "exported"
 	return $ ExportHandle h u
 
 closeDb :: ExportHandle -> Annex ()
diff --git a/Database/Fsck.hs b/Database/Fsck.hs
--- a/Database/Fsck.hs
+++ b/Database/Fsck.hs
@@ -76,7 +76,7 @@
 		initDb db $ void $
 			runMigrationSilent migrateFsck
 	lockFileCached =<< fromRepo (gitAnnexFsckDbLock u)
-	h <- liftIO $ H.openDbQueue H.MultiWriter db "fscked"
+	h <- liftIO $ H.openDbQueue db "fscked"
 	return $ FsckHandle h u
 
 closeDb :: FsckHandle -> Annex ()
diff --git a/Database/Handle.hs b/Database/Handle.hs
--- a/Database/Handle.hs
+++ b/Database/Handle.hs
@@ -9,7 +9,6 @@
 
 module Database.Handle (
 	DbHandle,
-	DbConcurrency(..),
 	openDb,
 	TableName,
 	queryDb,
@@ -37,46 +36,27 @@
 
 {- A DbHandle is a reference to a worker thread that communicates with
  - the database. It has a MVar which Jobs are submitted to. -}
-data DbHandle = DbHandle DbConcurrency (Async ()) (MVar Job)
+data DbHandle = DbHandle (Async ()) (MVar Job)
 
 {- Name of a table that should exist once the database is initialized. -}
 type TableName = String
 
-{- Sqlite only allows a single write to a database at a time; a concurrent
- - write will crash. 
- - 
- - MultiWrter works around this limitation.
- - The downside of using MultiWriter is that after writing a change to the
- - database, the a query using the same DbHandle will not immediately see
- - the change! This is because the change is actually written using a
- - separate database connection, and caching can prevent seeing the change.
- - Also, consider that if multiple processes are writing to a database,
- - you can't rely on seeing values you've just written anyway, as another
- - process may change them.
- -
- - When a database can only be written to by a single process (enforced by
- - a lock file), use SingleWriter. Changes written to the database will
- - always be immediately visible then. Multiple threads can write; their
- - writes will be serialized.
- -}
-data DbConcurrency = SingleWriter | MultiWriter
-
 {- Opens the database, but does not perform any migrations. Only use
  - once the database is known to exist and have the right tables. -}
-openDb :: DbConcurrency -> RawFilePath -> TableName -> IO DbHandle
-openDb dbconcurrency db tablename = do
+openDb :: RawFilePath -> TableName -> IO DbHandle
+openDb db tablename = do
 	jobs <- newEmptyMVar
 	worker <- async (workerThread (T.pack (fromRawFilePath db)) tablename jobs)
 	
 	-- work around https://github.com/yesodweb/persistent/issues/474
 	liftIO $ fileEncoding stderr
 
-	return $ DbHandle dbconcurrency worker jobs
+	return $ DbHandle worker jobs
 
 {- This is optional; when the DbHandle gets garbage collected it will
  - auto-close. -}
 closeDb :: DbHandle -> IO ()
-closeDb (DbHandle _ worker jobs) = do
+closeDb (DbHandle worker jobs) = do
 	putMVar jobs CloseJob
 	wait worker
 
@@ -89,12 +69,9 @@
  - Only one action can be run at a time against a given DbHandle.
  - If called concurrently in the same process, this will block until
  - it is able to run.
- -
- - Note that when the DbHandle was opened in MultiWriter mode, recent
- - writes may not be seen by queryDb.
  -}
 queryDb :: DbHandle -> SqlPersistM a -> IO a
-queryDb (DbHandle _ _ jobs) a = do
+queryDb (DbHandle _ jobs) a = do
 	res <- newEmptyMVar
 	putMVar jobs $ QueryJob $
 		liftIO . putMVar res =<< tryNonAsync a
@@ -103,10 +80,9 @@
 
 {- Writes a change to the database.
  -
- - In MultiWriter mode, writes can fail if another write is happening
- - concurrently. So write failures are caught and retried repeatedly
- - for up to 10 seconds, which should avoid all but the most exceptional
- - problems.
+ - Writes can fail if another write is happening concurrently.
+ - So write failures are caught and retried repeatedly for up to 10
+ - seconds, which should avoid all but the most exceptional problems.
  -}
 commitDb :: DbHandle -> SqlPersistM () -> IO ()
 commitDb h wa = robustly Nothing 100 (commitDb' h wa)
@@ -122,59 +98,44 @@
 				robustly (Just e) (n-1) a
 
 commitDb' :: DbHandle -> SqlPersistM () -> IO (Either SomeException ())
-commitDb' (DbHandle MultiWriter _ jobs) a = do
-	res <- newEmptyMVar
-	putMVar jobs $ RobustChangeJob $ \runner ->
-		liftIO $ putMVar res =<< tryNonAsync (runner a)
-	takeMVar res
-commitDb' (DbHandle SingleWriter _ jobs) a = do
+commitDb' (DbHandle _ jobs) a = do
 	res <- newEmptyMVar
 	putMVar jobs $ ChangeJob $
 		liftIO . putMVar res =<< tryNonAsync a
 	takeMVar res
-		`catchNonAsync` (const $ error "sqlite commit crashed")
 
 data Job
 	= QueryJob (SqlPersistM ())
 	| ChangeJob (SqlPersistM ())
-	| RobustChangeJob ((SqlPersistM () -> IO ()) -> IO ())
 	| CloseJob
 
 workerThread :: T.Text -> TableName -> MVar Job -> IO ()
-workerThread db tablename jobs = go
+workerThread db tablename jobs = newconn
   where
-	go = do
+	newconn = do
 		v <- tryNonAsync (runSqliteRobustly tablename db loop)
 		case v of
 			Left e -> hPutStrLn stderr $
 				"sqlite worker thread crashed: " ++ show e
-			Right True -> go
-			Right False -> return ()
+			Right cont -> cont
 	
-	getjob :: IO (Either BlockedIndefinitelyOnMVar Job)
-	getjob = try $ takeMVar jobs
-
 	loop = do
 		job <- liftIO getjob
 		case job of
 			-- Exception is thrown when the MVar is garbage
 			-- collected, which means the whole DbHandle
 			-- is not used any longer. Shutdown cleanly.
-			Left BlockedIndefinitelyOnMVar -> return False
-			Right CloseJob -> return False
+			Left BlockedIndefinitelyOnMVar -> return (return ())
+			Right CloseJob -> return (return ())
 			Right (QueryJob a) -> a >> loop
 			Right (ChangeJob a) -> do
 				a
-				-- Exit this sqlite transaction so the
+				-- Exit the sqlite connection so the
 				-- database gets updated on disk.
-				return True
-			-- Change is run in a separate database connection
-			-- since sqlite only supports a single writer at a
-			-- time, and it may crash the database connection
-			-- that the write is made to.
-			Right (RobustChangeJob a) -> do
-				liftIO (a (runSqliteRobustly tablename db))
-				loop
+				return newconn
+	
+	getjob :: IO (Either BlockedIndefinitelyOnMVar Job)
+	getjob = try $ takeMVar jobs
 	
 -- Like runSqlite, but more robust.
 --
diff --git a/Database/Keys.hs b/Database/Keys.hs
--- a/Database/Keys.hs
+++ b/Database/Keys.hs
@@ -37,7 +37,7 @@
 import Annex.LockFile
 import Annex.Content.PointerFile
 import Annex.Content.Presence.LowLevel
-import Annex.Link
+import Annex.Link (Restage(..), maxPointerSz, parseLinkTargetOrPointerLazy)
 import Utility.InodeCache
 import Annex.InodeSentinal
 import Git
@@ -134,7 +134,7 @@
 		| otherwise = return DbUnavailable
 	
 	open db = do
-		qh <- liftIO $ H.openDbQueue H.MultiWriter db SQL.containedTable
+		qh <- liftIO $ H.openDbQueue db SQL.containedTable
 		reconcileStaged qh
 		return $ DbOpen qh
 
diff --git a/Database/Queue.hs b/Database/Queue.hs
--- a/Database/Queue.hs
+++ b/Database/Queue.hs
@@ -9,7 +9,6 @@
 
 module Database.Queue (
 	DbQueue,
-	DbConcurrency(..),
 	openDbQueue,
 	queryDbQueue,
 	closeDbQueue,
@@ -37,9 +36,9 @@
 {- Opens the database queue, but does not perform any migrations. Only use
  - if the database is known to exist and have the right tables; ie after
  - running initDb. -}
-openDbQueue :: DbConcurrency -> RawFilePath -> TableName -> IO DbQueue
-openDbQueue dbconcurrency db tablename = DQ
-	<$> openDb dbconcurrency db tablename
+openDbQueue :: RawFilePath -> TableName -> IO DbQueue
+openDbQueue db tablename = DQ
+	<$> openDb db tablename
 	<*> (newMVar =<< emptyQueue)
 
 {- This or flushDbQueue must be called, eg at program exit to ensure
@@ -64,9 +63,6 @@
  -
  - Queries will not see changes that have been recently queued,
  - so use with care.
- -
- - Also, when the database was opened in MultiWriter mode,
- - queries may not see changes even after flushDbQueue.
  -}
 queryDbQueue :: DbQueue -> SqlPersistM a -> IO a
 queryDbQueue (DQ hdl _) = queryDb hdl
diff --git a/Git/Construct.hs b/Git/Construct.hs
--- a/Git/Construct.hs
+++ b/Git/Construct.hs
@@ -184,7 +184,10 @@
 #ifdef mingw32_HOST_OS
 expandTilde = return
 #else
-expandTilde = expandt True
+expandTilde p = expandt True p
+	-- If unable to expand a tilde, eg due to a user not existing,
+	-- use the path as given.
+	`catchNonAsync` (const (return p))
   where
 	expandt _ [] = return ""
 	expandt _ ('/':cs) = do
diff --git a/Remote/GCrypt.hs b/Remote/GCrypt.hs
--- a/Remote/GCrypt.hs
+++ b/Remote/GCrypt.hs
@@ -396,8 +396,7 @@
 		then fileStorer $ \k f p -> do
 			oh <- mkOutputHandler
 			ok <- Ssh.rsyncHelper oh (Just p)
-				=<< Ssh.rsyncParamsRemote False r Upload k f
-					(AssociatedFile Nothing)
+				=<< Ssh.rsyncParamsRemote r Upload k f
 			unless ok $
 				giveup "rsync failed"
 		else storersync
@@ -418,9 +417,8 @@
 			sink =<< liftIO (L.readFile $ gCryptLocation repo k)
 	| Git.repoIsSsh repo = if accessShell r
 		then fileRetriever $ \f k p -> do
-			ps <- Ssh.rsyncParamsRemote False r Download k
+			ps <- Ssh.rsyncParamsRemote r Download k
 				(fromRawFilePath f)
-				(AssociatedFile Nothing)
 			oh <- mkOutputHandler
 			unlessM (Ssh.rsyncHelper oh (Just p) ps) $
 				giveup "rsync failed"
diff --git a/Remote/Git.hs b/Remote/Git.hs
--- a/Remote/Git.hs
+++ b/Remote/Git.hs
@@ -48,7 +48,6 @@
 import Utility.Metered
 import Utility.Env
 import Utility.Batch
-import Utility.SimpleProtocol
 import Remote.Helper.Git
 import Remote.Helper.Messages
 import Remote.Helper.ExportImport
@@ -70,7 +69,6 @@
 #endif
 
 import Control.Concurrent
-import Control.Concurrent.MSampleVar
 import qualified Data.Map as M
 import qualified Data.ByteString as S
 import Network.URI
@@ -413,9 +411,7 @@
 			( return True
 			, giveup "not found"
 			)
-	checkremote = 
-		let fallback = Ssh.inAnnex repo key
-		in P2PHelper.checkpresent (Ssh.runProto rmt connpool (cantCheck rmt) fallback) key
+	checkremote = P2PHelper.checkpresent (Ssh.runProto rmt connpool (cantCheck rmt)) key
 	checklocal = ifM duc
 		( guardUsable repo (cantCheck repo) $
 			maybe (cantCheck repo) return
@@ -458,9 +454,7 @@
 		, giveup "remote does not have expected annex.uuid value"
 		)
 	| Git.repoIsHttp repo = giveup "dropping from http remote not supported"
-	| otherwise = commitOnCleanup repo r st $ do
-		let fallback = Ssh.dropKey' repo key
-		P2PHelper.remove (Ssh.runProto r connpool (return False) fallback) key
+	| otherwise = P2PHelper.remove (Ssh.runProto r connpool (return False)) key
 
 lockKey :: Remote -> State -> Key -> (VerifiedCopy -> Annex r) -> Annex r
 lockKey r st key callback = do
@@ -482,63 +476,20 @@
 		)
 	| Git.repoIsSsh repo = do
 		showLocking r
-		let withconn = Ssh.withP2PSshConnection r connpool fallback
+		let withconn = Ssh.withP2PSshConnection r connpool failedlock
 		P2PHelper.lock withconn Ssh.runProtoConn (uuid r) key callback
 	| otherwise = failedlock
   where
-	fallback = withNullHandle $ \nullh -> do
-		Just (cmd, params) <- Ssh.git_annex_shell ConsumeStdin
-			repo "lockcontent"
-			[Param $ serializeKey key] []
-		let p = (proc cmd (toCommand params))
-			{ std_in = CreatePipe
-			, std_out = CreatePipe
-			, std_err = UseHandle nullh
-			}
-		bracketIO (createProcess p) cleanupProcess fallback'
-
-	fallback' (Just hin, Just hout, Nothing, p) = do
-		v <- liftIO $ tryIO $ getProtocolLine hout
-		let signaldone = void $ tryNonAsync $ liftIO $ mapM_ tryNonAsync
-			[ hPutStrLn hout ""
-			, hFlush hout
-			, hClose hin
-			, hClose hout
-			, void $ waitForProcess p
-			]
-		let checkexited = not . isJust <$> getProcessExitCode p
-		case v of
-			Left _exited -> do
-				showNote "lockcontent failed"
-				liftIO $ do
-					hClose hin
-					hClose hout
-					void $ waitForProcess p
-				failedlock
-			Right l 
-				| l == Just Ssh.contentLockedMarker -> bracket_
-					noop
-					signaldone 
-					(withVerifiedCopy LockedCopy r checkexited callback)
-				| otherwise -> do
-					showNote "lockcontent failed"
-					signaldone
-					failedlock
-	fallback' _ = error "internal"
-
 	failedlock = giveup "can't lock content"
 
 {- Tries to copy a key's content from a remote's annex to a file. -}
 copyFromRemote :: Remote -> State -> Key -> AssociatedFile -> FilePath -> MeterUpdate -> VerifyConfig -> Annex Verification
-copyFromRemote = copyFromRemote' False
-
-copyFromRemote' :: Bool -> Remote -> State -> Key -> AssociatedFile -> FilePath -> MeterUpdate -> VerifyConfig -> Annex Verification
-copyFromRemote' forcersync r st key file dest meterupdate vc = do
+copyFromRemote r st key file dest meterupdate vc = do
 	repo <- getRepo r
-	copyFromRemote'' repo forcersync r st key file dest meterupdate vc
+	copyFromRemote'' repo r st key file dest meterupdate vc
 
-copyFromRemote'' :: Git.Repo -> Bool -> Remote -> State -> Key -> AssociatedFile -> FilePath -> MeterUpdate -> VerifyConfig -> Annex Verification
-copyFromRemote'' repo forcersync r st@(State connpool _ _ _ _) key file dest meterupdate vc
+copyFromRemote'' :: Git.Repo -> Remote -> State -> Key -> AssociatedFile -> FilePath -> MeterUpdate -> VerifyConfig -> Annex Verification
+copyFromRemote'' repo r st@(State connpool _ _ _ _) key file dest meterupdate vc
 	| Git.repoIsHttp repo = do
 		iv <- startVerifyKeyContentIncrementally vc key
 		gc <- Annex.getGitConfig
@@ -566,91 +517,13 @@
 					then return v
 					else giveup "failed to retrieve content from remote"
 			Nothing -> giveup "content is not present in remote"
-	| Git.repoIsSsh repo = if forcersync
-		then do
-			(ok, v) <- fallback meterupdate
-			if ok
-				then return v
-				else giveup "failed to retrieve content from remote"
-		else P2PHelper.retrieve
+	| Git.repoIsSsh repo =
+		P2PHelper.retrieve
 			(gitconfig r)
-			(\p -> Ssh.runProto r connpool (return (False, UnVerified)) (fallback p))
+			(Ssh.runProto r connpool (return (False, UnVerified)))
 			key file dest meterupdate vc
 	| otherwise = giveup "copying from non-ssh, non-http remote not supported"
-  where
-	fallback p = unVerified $ feedprogressback $ \p' -> do
-		oh <- mkOutputHandlerQuiet
-		Ssh.rsyncHelper oh (Just (combineMeterUpdate p' p))
-			=<< Ssh.rsyncParamsRemote False r Download key dest file
 
-	{- Feed local rsync's progress info back to the remote,
-	 - by forking a feeder thread that runs
-	 - git-annex-shell transferinfo at the same time
-	 - git-annex-shell sendkey is running.
-	 -
-	 - To avoid extra password prompts, this is only done when ssh
-	 - connection caching is supported.
-	 - Note that it actually waits for rsync to indicate
-	 - progress before starting transferinfo, in order
-	 - to ensure ssh connection caching works and reuses 
-	 - the connection set up for the sendkey.
-	 -
-	 - Also note that older git-annex-shell does not support
-	 - transferinfo, so stderr is dropped and failure ignored.
-	 -}
-	feedprogressback a = ifM (isJust <$> sshCacheDir)
-		( feedprogressback' a
-		, a $ const noop
-		)
-	feedprogressback' a = do
-		u <- getUUID
-		let AssociatedFile afile = file
-		let fields = (Fields.remoteUUID, fromUUID u)
-			: maybe [] (\f -> [(Fields.associatedFile, fromRawFilePath f)]) afile
-		Just (cmd, params) <- Ssh.git_annex_shell ConsumeStdin
-			repo "transferinfo" 
-			[Param $ serializeKey key] fields
-		v <- liftIO (newEmptySV :: IO (MSampleVar Integer))
-		pv <- liftIO $ newEmptyMVar
-		tid <- liftIO $ forkIO $ void $ tryIO $ do
-			bytes <- readSV v
-			p <- createProcess $
-				 (proc cmd (toCommand params))
-					{ std_in = CreatePipe
-					, std_err = CreatePipe
-					}
-			putMVar pv p
-			hClose $ stderrHandle p
-			let h = stdinHandle p
-			let send b = do
-				hPrint h b
-				hFlush h
-			send bytes
-			forever $
-				send =<< readSV v
-		let feeder = \n -> do
-			meterupdate n
-			writeSV v (fromBytesProcessed n)
-
-		-- It can easily take 0.3 seconds to clean up after
-		-- the transferinfo, and all that's involved is shutting
-		-- down the process and associated thread cleanly. So,
-		-- do it in the background.
-		let cleanup = forkIO $ do
-			void $ tryIO $ killThread tid
-			void $ tryNonAsync $ 
-				maybe noop (void . waitForProcess . processHandle)
-					=<< tryTakeMVar pv
-
-		let forcestop = do
-			void $ tryIO $ killThread tid
-			void $ tryNonAsync $ 
-				maybe noop cleanupProcess
-					=<< tryTakeMVar pv
-
-		bracketIO noop (const cleanup) (const $ a feeder)
-			`onException` liftIO forcestop
-
 copyFromRemoteCheap :: State -> Git.Repo -> Maybe (Key -> AssociatedFile -> FilePath -> Annex ())
 #ifndef mingw32_HOST_OS
 copyFromRemoteCheap st repo
@@ -681,9 +554,9 @@
 			copylocal =<< Annex.Content.prepSendAnnex' key
 		, giveup "remote does not have expected annex.uuid value"
 		)
-	| Git.repoIsSsh repo = commitOnCleanup repo r st $
+	| Git.repoIsSsh repo =
 		P2PHelper.store (gitconfig r)
-			(Ssh.runProto r connpool (return False) . copyremotefallback)
+			(Ssh.runProto r connpool (return False))
 			key file meterupdate
 		
 	| otherwise = giveup "copying to non-ssh repo not supported"
@@ -715,24 +588,10 @@
 			)
 		unless res $
 			giveup "failed to send content to remote"
-	copyremotefallback p = either (const False) id
-		<$> tryNonAsync (copyremotefallback' p)
-	copyremotefallback' p = Annex.Content.sendAnnex key noop $ \object -> do
-		-- This is too broad really, but recvkey normally
-		-- verifies content anyway, so avoid complicating
-		-- it with a local sendAnnex check and rollback.
-		let unlocked = True
-		oh <- mkOutputHandlerQuiet
-		Ssh.rsyncHelper oh (Just p)
-			=<< Ssh.rsyncParamsRemote unlocked r Upload key object file
 
 fsckOnRemote :: Git.Repo -> [CommandParam] -> Annex (IO Bool)
 fsckOnRemote r params
-	| Git.repoIsUrl r = do
-		s <- Ssh.git_annex_shell NoConsumeStdin r "fsck" params []
-		return $ case s of
-			Nothing -> return False
-			Just (c, ps) -> batchCommand c ps
+	| Git.repoIsUrl r = return $ return False
 	| otherwise = return $ do
 		program <- programPath
 		r' <- Git.Config.read r
@@ -820,21 +679,7 @@
 		| not $ Git.repoIsUrl repo = onLocalFast st $
 			doQuietSideAction $
 				Annex.Branch.commit =<< Annex.Branch.commitMessage
-		| otherwise = do
-			Just (shellcmd, shellparams) <-
-				Ssh.git_annex_shell NoConsumeStdin
-					repo "commit" [] []
-			
-			-- Throw away stderr, since the remote may not
-			-- have a new enough git-annex shell to
-			-- support committing.
-			liftIO $ void $ catchMaybeIO $ withNullHandle $ \nullh ->
-				let p = (proc shellcmd (toCommand shellparams))
-					{ std_out = UseHandle nullh
-					, std_err = UseHandle nullh
-					}
-				in withCreateProcess p $ \_ _ _ ->
-					forceSuccessProcess p
+		| otherwise = noop
 
 wantHardLink :: Annex Bool
 wantHardLink = (annexHardLink <$> Annex.getGitConfig)
diff --git a/Remote/Helper/Chunked.hs b/Remote/Helper/Chunked.hs
--- a/Remote/Helper/Chunked.hs
+++ b/Remote/Helper/Chunked.hs
@@ -286,9 +286,9 @@
 	firstavail Nothing _ [] = giveup "unable to determine the chunks to use for this remote"
 	firstavail (Just e) _ [] = throwM e
 	firstavail pe currsize ([]:ls) = firstavail pe currsize ls
-	firstavail _ currsize ((k:ks):ls)
+	firstavail pe currsize ((k:ks):ls)
 		| k == basek = getunchunked
-			`catchNonAsync` (\e -> firstavail (Just e) currsize ls)
+			`catchNonAsync` (\e -> firstavail (Just (pickerr e)) currsize ls)
 		| otherwise = do
 			let offset = resumeOffset currsize k
 			let p = maybe basep
@@ -302,10 +302,15 @@
 							fromMaybe 0 $ fromKey keyChunkSize k
 						getrest p h iv sz sz ks
 			case v of
-				Left e
-					| null ls -> throwM e
-					| otherwise -> firstavail (Just e) currsize ls
+				Left e -> firstavail (Just (pickerr e)) currsize ls
 				Right r -> return r
+	  where
+		-- Prefer an earlier exception to a later one, because the
+		-- more probable location is tried first and less probable
+		-- ones later.
+		pickerr e = case pe of
+			Just pe' -> pe'
+			Nothing -> e
 
 	getrest _ _ iv _ _ [] = return (Right iv)
 	getrest p h iv sz bytesprocessed (k:ks) = do
diff --git a/Remote/Helper/P2P.hs b/Remote/Helper/P2P.hs
--- a/Remote/Helper/P2P.hs
+++ b/Remote/Helper/P2P.hs
@@ -31,22 +31,22 @@
 -- the pool when done.
 type WithConn a c = (ClosableConnection c -> Annex (ClosableConnection c, a)) -> Annex a
 
-store :: RemoteGitConfig -> (MeterUpdate -> ProtoRunner Bool) -> Key -> AssociatedFile -> MeterUpdate -> Annex ()
+store :: RemoteGitConfig -> ProtoRunner Bool -> Key -> AssociatedFile -> MeterUpdate -> Annex ()
 store gc runner k af p = do
 	let sizer = KeySizer k (fmap (toRawFilePath . fst) <$> prepSendAnnex k)
 	let bwlimit = remoteAnnexBwLimit gc
 	metered (Just p) sizer bwlimit $ \_ p' ->
-		runner p' (P2P.put k af p') >>= \case
+		runner (P2P.put k af p') >>= \case
 			Just True -> return ()
 			Just False -> giveup "Transfer failed"
 			Nothing -> remoteUnavail
 
-retrieve :: RemoteGitConfig -> (MeterUpdate -> ProtoRunner (Bool, Verification)) -> Key -> AssociatedFile -> FilePath -> MeterUpdate -> VerifyConfig -> Annex Verification
+retrieve :: RemoteGitConfig -> (ProtoRunner (Bool, Verification)) -> Key -> AssociatedFile -> FilePath -> MeterUpdate -> VerifyConfig -> Annex Verification
 retrieve gc runner k af dest p verifyconfig = do
 	iv <- startVerifyKeyContentIncrementally verifyconfig k
 	let bwlimit = remoteAnnexBwLimit gc
 	metered (Just p) k bwlimit $ \m p' -> 
-		runner p' (P2P.get dest k iv af m p') >>= \case
+		runner (P2P.get dest k iv af m p') >>= \case
 			Just (True, v) -> return v
 			Just (False, _) -> giveup "Transfer failed"
 			Nothing -> remoteUnavail
diff --git a/Remote/Helper/Ssh.hs b/Remote/Helper/Ssh.hs
--- a/Remote/Helper/Ssh.hs
+++ b/Remote/Helper/Ssh.hs
@@ -130,20 +130,14 @@
 
 {- Generates rsync parameters that ssh to the remote and asks it
  - to either receive or send the key's content. -}
-rsyncParamsRemote :: Bool -> Remote -> Direction -> Key -> FilePath -> AssociatedFile -> Annex [CommandParam]
-rsyncParamsRemote unlocked r direction key file (AssociatedFile afile) = do
+rsyncParamsRemote :: Remote -> Direction -> Key -> FilePath -> Annex [CommandParam]
+rsyncParamsRemote r direction key file = do
 	u <- getUUID
-	let fields = (Fields.remoteUUID, fromUUID u)
-		: (Fields.unlocked, if unlocked then "1" else "")
-		-- Send direct field for unlocked content, for backwards
-		-- compatability.
-		: (Fields.direct, if unlocked then "1" else "")
-		: maybe [] (\f -> [(Fields.associatedFile, fromRawFilePath f)]) afile
 	repo <- getRepo r
 	Just (shellcmd, shellparams) <- git_annex_shell ConsumeStdin repo
 		(if direction == Download then "sendkey" else "recvkey")
 		[ Param $ serializeKey key ]
-		fields
+		[(Fields.remoteUUID, fromUUID u)]
 	-- Convert the ssh command into rsync command line.
 	let eparam = rsyncShell (Param shellcmd:shellparams)
 	o <- rsyncParams r direction
@@ -183,11 +177,6 @@
 		| otherwise = remoteAnnexRsyncUploadOptions gc
 	gc = gitconfig r
 
--- Used by git-annex-shell lockcontent to indicate the content is
--- successfully locked.
-contentLockedMarker :: String
-contentLockedMarker = "OK"
-
 -- A connection over ssh to git-annex shell speaking the P2P protocol.
 type P2PSshConnection = P2P.ClosableConnection
 	(P2P.RunState, P2P.P2PConnection, ProcessHandle, TVar StderrHandlerState)
@@ -320,9 +309,9 @@
 
 -- Runs a P2P Proto action on a remote when it supports that,
 -- otherwise the fallback action.
-runProto :: Remote -> P2PSshConnectionPool -> Annex a -> Annex a -> P2P.Proto a -> Annex (Maybe a)
-runProto r connpool badproto fallback proto = Just <$>
-	(getP2PSshConnection r connpool >>= maybe fallback go)
+runProto :: Remote -> P2PSshConnectionPool -> Annex a -> P2P.Proto a -> Annex (Maybe a)
+runProto r connpool onerr proto = Just <$>
+	(getP2PSshConnection r connpool >>= maybe onerr go)
   where
 	go c = do
 		(c', v) <- runProtoConn proto c
@@ -330,9 +319,7 @@
 			Just res -> do
 				liftIO $ storeP2PSshConnection connpool c'
 				return res
-			-- Running the proto failed, either due to a protocol
-			-- error or a network error.
-			Nothing -> badproto
+			Nothing -> onerr
 
 runProtoConn :: P2P.Proto a -> P2PSshConnection -> Annex (P2PSshConnection, Maybe a)
 runProtoConn _ P2P.ClosedConnection = return (P2P.ClosedConnection, Nothing)
diff --git a/Remote/P2P.hs b/Remote/P2P.hs
--- a/Remote/P2P.hs
+++ b/Remote/P2P.hs
@@ -55,8 +55,8 @@
 		{ uuid = u
 		, cost = cst
 		, name = Git.repoDescribe r
-		, storeKey = store gc (const protorunner)
-		, retrieveKeyFile = retrieve gc (const protorunner)
+		, storeKey = store gc protorunner
+		, retrieveKeyFile = retrieve gc protorunner
 		, retrieveKeyFileCheap = Nothing
 		, retrievalSecurityPolicy = RetrievalAllKeysSecure
 		, removeKey = remove protorunner
@@ -148,14 +148,7 @@
 			authtoken <- fromMaybe nullAuthToken
 				<$> loadP2PRemoteAuthToken addr
 			let proto = P2P.auth myuuid authtoken $
-				-- Before 6.20180312, the protocol server
-				-- had a bug that made negotiating the
-				-- protocol version terminate the
-				-- connection. So, this must stay disabled
-				-- until the old version is not in use
-				-- anywhere.
-				--P2P.negotiateProtocolVersion P2P.maxProtocolVersion
-				return ()
+				P2P.negotiateProtocolVersion P2P.maxProtocolVersion
 			runst <- liftIO $ mkRunState Client
 			res <- liftIO $ runNetProto runst conn proto
 			case res of
diff --git a/RemoteDaemon/Transport/Ssh.hs b/RemoteDaemon/Transport/Ssh.hs
--- a/RemoteDaemon/Transport/Ssh.hs
+++ b/RemoteDaemon/Transport/Ssh.hs
@@ -42,15 +42,13 @@
 	p = (proc cmd (toCommand params))
 		{ std_in = CreatePipe
 		, std_out = CreatePipe
-		, std_err = CreatePipe
 		}
 
-	go (Just toh) (Just fromh) (Just errh) pid = do
+	go (Just toh) (Just fromh) Nothing pid = do
 		-- Run all threads until one finishes and get the status
 		-- of the first to finish. Cancel the rest.
 		status <- catchDefaultIO (Right ConnectionClosed) $
-			handlestderr errh
-				`race` handlestdout fromh
+				handlestdout fromh
 					`race` handlecontrol
 
 		send (DISCONNECTED url)
@@ -58,7 +56,7 @@
 		hClose fromh
 		void $ waitForProcess pid
 
-		return $ either (either id id) id status
+		return $ either id id status
 	go _ _ _ _ = error "internal"
 
 	send msg = atomically $ writeTChan ochan msg
@@ -88,21 +86,3 @@
 			STOP -> return ConnectionStopping
 			LOSTNET -> return ConnectionStopping
 			_ -> handlecontrol
-
-	-- Old versions of git-annex-shell that do not support
-	-- the notifychanges command will exit with a not very useful
-	-- error message. Detect that error, and avoid reconnecting.
-	-- Propigate all stderr.
-	handlestderr errh = do
-		s <- hGetSomeString errh 1024
-		hPutStr stderr s
-		hFlush stderr
-		if "git-annex-shell: git-shell failed" `isInfixOf` s
-			then do
-				send $ WARNING url $ unwords
-					[ "Remote", Git.repoDescribe r
-					, "needs its git-annex upgraded"
-					, "to 5.20140405 or newer"
-					]
-				return ConnectionStopping
-			else handlestderr errh
diff --git a/Test.hs b/Test.hs
--- a/Test.hs
+++ b/Test.hs
@@ -194,6 +194,7 @@
 	, testProperty "prop_upFrom_basics" Utility.Path.Tests.prop_upFrom_basics
 	, testProperty "prop_relPathDirToFileAbs_basics" Utility.Path.Tests.prop_relPathDirToFileAbs_basics
 	, testProperty "prop_relPathDirToFileAbs_regressionTest" Utility.Path.Tests.prop_relPathDirToFileAbs_regressionTest
+	, testProperty "prop_dirContains_regressionTest" Utility.Path.Tests.prop_dirContains_regressionTest
 	, testProperty "prop_cost_sane" Config.Cost.prop_cost_sane
 	, testProperty "prop_matcher_sane" Utility.Matcher.prop_matcher_sane
 	, testProperty "prop_HmacSha1WithCipher_sane" Crypto.prop_HmacSha1WithCipher_sane
@@ -1821,13 +1822,20 @@
 	testscheme "pubkey"
   where
 	gpgcmd = Utility.Gpg.mkGpgCmd Nothing
-	testscheme scheme = do
-		abstmp <- fromRawFilePath <$> absPath (toRawFilePath tmpdir)
-		testscheme' scheme abstmp
-	testscheme' scheme abstmp = intmpclonerepo $ do
-		gpgtmp <- (</> "gpgtmp") . fromRawFilePath
-			<$> relPathCwdToFile (toRawFilePath abstmp)
-		createDirectoryIfMissing False gpgtmp
+	testscheme scheme = Utility.Tmp.Dir.withTmpDir "gpgtmp" $ \gpgtmp -> do
+		-- Use the system temp directory as gpg temp directory because 
+		-- it needs to be able to store the agent socket there,
+		-- which can be problimatic when testing some filesystems.
+		absgpgtmp <- fromRawFilePath <$> absPath (toRawFilePath gpgtmp)
+		testscheme' scheme absgpgtmp
+	testscheme' scheme absgpgtmp = intmpclonerepo $ do
+		-- Since gpg uses a unix socket, which is limited to a
+		-- short path, use whichever is shorter of absolute
+		-- or relative path.
+		relgpgtmp <- fromRawFilePath <$> relPathCwdToFile (toRawFilePath absgpgtmp)
+		let gpgtmp = if length relgpgtmp < length absgpgtmp
+			then relgpgtmp 
+			else absgpgtmp
 		Utility.Gpg.testTestHarness gpgtmp gpgcmd
 			@? "test harness self-test failed"
 		void $ Utility.Gpg.testHarness gpgtmp gpgcmd $ do
diff --git a/Utility/InodeCache.hs b/Utility/InodeCache.hs
--- a/Utility/InodeCache.hs
+++ b/Utility/InodeCache.hs
@@ -55,7 +55,7 @@
 #ifdef mingw32_HOST_OS
 import Data.Word (Word64)
 #else
-import System.Posix.Files
+import qualified System.Posix.Files as Posix
 #endif
 
 data InodeCachePrim = InodeCachePrim FileID FileSize MTime
@@ -200,7 +200,7 @@
 #ifdef mingw32_HOST_OS
 		mtime <- utcTimeToPOSIXSeconds <$> getModificationTime (fromRawFilePath f)
 #else
-		let mtime = modificationTimeHiRes s
+		let mtime = Posix.modificationTimeHiRes s
 #endif
 		return $ Just $ InodeCache $ InodeCachePrim inode sz (MTimeHighRes (mtime + highResTime delta))
 	| otherwise = pure Nothing
@@ -299,11 +299,6 @@
                 [ (50, MTimeLowRes <$> (abs . fromInteger <$> arbitrary))
                 , (50, MTimeHighRes <$> arbitrary)
 		]
-
-#ifdef mingw32_HOST_OS
-instance Arbitrary FileID where
-	arbitrary = fromIntegral <$> (arbitrary :: Gen Word64)
-#endif
 
 prop_read_show_inodecache :: InodeCache -> Bool
 prop_read_show_inodecache c = case readInodeCache (showInodeCache c) of
diff --git a/Utility/LinuxMkLibs.hs b/Utility/LinuxMkLibs.hs
--- a/Utility/LinuxMkLibs.hs
+++ b/Utility/LinuxMkLibs.hs
@@ -69,7 +69,7 @@
  - XXX Debian specific. -}
 glibcLibs :: IO [FilePath]
 glibcLibs = lines <$> readProcess "sh"
-	["-c", "dpkg -L libc6:$(dpkg --print-architecture) libgcc1:$(dpkg --print-architecture) | egrep '\\.so' | grep -v /gconv/ | grep -v ld.so.conf | grep -v sotruss-lib"]
+	["-c", "dpkg -L libc6:$(dpkg --print-architecture) | egrep '\\.so' | grep -v /gconv/ | grep -v ld.so.conf | grep -v sotruss-lib"]
 
 {- Get gblibc's gconv libs, which are handled specially.. -}
 gconvLibs :: IO [FilePath]
diff --git a/Utility/Path.hs b/Utility/Path.hs
--- a/Utility/Path.hs
+++ b/Utility/Path.hs
@@ -97,6 +97,7 @@
 	|| a' == b'
 	|| (a'' `B.isPrefixOf` b' && avoiddotdotb)
 	|| a' == "." && normalise ("." </> b') == b' && nodotdot b'
+	|| dotdotcontains
   where
 	a' = norm a
 	a'' = addTrailingPathSeparator a'
@@ -115,9 +116,27 @@
 	 -}
 	avoiddotdotb = nodotdot $ B.drop (B.length a'') b'
 
-	nodotdot p = all
-		(\s -> dropTrailingPathSeparator s /= "..")
-		(splitPath p)
+	nodotdot p = all (not . isdotdot) (splitPath p)
+	
+	isdotdot s = dropTrailingPathSeparator s == ".."
+
+	{- This handles the case where a is ".." or "../.." etc,
+	 - and b is "foo" or "../foo" etc. The rule is that when
+	 - a is entirely ".." components, b is under it when it starts
+	 - with fewer ".." components. 
+	 - 
+	 - Due to the use of norm, cases like "../../foo/../../" get
+	 - converted to eg "../../../" and so do not need to be handled
+	 - specially here.
+	 -}
+	dotdotcontains
+		| isAbsolute b' = False
+		| otherwise = 
+			let aps = splitPath a'
+			    bps = splitPath b'
+			in if all isdotdot aps
+				then length (takeWhile isdotdot bps) < length aps
+				else False
 
 {- Given an original list of paths, and an expanded list derived from it,
  - which may be arbitrarily reordered, generates a list of lists, where
diff --git a/Utility/Path/Tests.hs b/Utility/Path/Tests.hs
--- a/Utility/Path/Tests.hs
+++ b/Utility/Path/Tests.hs
@@ -14,6 +14,7 @@
 	prop_upFrom_basics,
 	prop_relPathDirToFileAbs_basics,
 	prop_relPathDirToFileAbs_regressionTest,
+	prop_dirContains_regressionTest,
 ) where
 
 import System.FilePath.ByteString
@@ -62,3 +63,18 @@
 		relPathDirToFileAbs (joinPath [pathSeparator `B.cons` "tmp", "r", "lll", "xxx", "yyy", "18"])
 			(joinPath [pathSeparator `B.cons` "tmp", "r", ".git", "annex", "objects", "18", "gk", "SHA256-foo", "SHA256-foo"])
 				== joinPath ["..", "..", "..", "..", ".git", "annex", "objects", "18", "gk", "SHA256-foo", "SHA256-foo"]
+
+prop_dirContains_regressionTest :: Bool
+prop_dirContains_regressionTest = and
+	[ not $ dirContains "." ".."
+	, not $ dirContains ".." "../.."
+	, dirContains "." "foo"
+	, dirContains "." "."
+	, dirContains ".." ".."
+	, dirContains "../.." "../.."
+	, dirContains "." "./foo"
+	, dirContains ".." "../foo"
+	, dirContains "../.." "../foo"
+	, dirContains "../.." "../../foo"
+	, not $ dirContains "../.." "../../.."
+	]
diff --git a/doc/git-annex-inprogress.mdwn b/doc/git-annex-inprogress.mdwn
--- a/doc/git-annex-inprogress.mdwn
+++ b/doc/git-annex-inprogress.mdwn
@@ -13,6 +13,9 @@
 name of the temporary file that is being used to download the specified
 annexed file.
 
+Nothing will be output when the download is from an encrypted or chunked 
+special remote.
+
 This can sometimes be used to stream a file before it's been fully
 downloaded, for example:
 
diff --git a/doc/git-annex-shell.mdwn b/doc/git-annex-shell.mdwn
--- a/doc/git-annex-shell.mdwn
+++ b/doc/git-annex-shell.mdwn
@@ -31,79 +31,58 @@
 
   When run in a repository that does not yet have an annex.uuid, one
   will be created, as long as a git-annex branch has already been pushed to
-  the repository, or if the autoinit= flag is used to indicate
+  the repository, or if the autoinit=1 flag is used to indicate
   initialization is desired.
 
-* inannex directory [key ...]
+* p2pstdio directory uuid
 
-  This checks if all specified keys are present in the annex, 
-  and exits zero if so.
+  This causes git-annex-shell to communicate using the git-annex p2p
+  protocol over stdio.
 
-  Exits 1 if the key is certainly not present in the annex.
-  Exits 100 if it's unable to tell (perhaps the key is in the process of
-  being removed from the annex).
+  The uuid is the one belonging to the repository that will be
+  communicating with git-annex-shell.
 
-* lockcontent directory key
+* notifychanges directory
 
-  This locks a key's content in place in the annex, preventing it from
-  being dropped.
+  This is used by `git-annex remotedaemon` to be notified when
+  refs in the remote repository are changed.
 
-  Once the content is successfully locked, outputs "OK". Then the content
-  remains locked until a newline is received from the caller or the
-  connection is broken.
+* gcryptsetup directory gcryptid
 
-  Exits nonzero if the content is not present, or could not be locked.
+  Sets up a repository as a gcrypt repository.
 
-* dropkey directory [key ...]
+* inannex directory [key ...]
 
-  This drops the annexed data for the specified keys.
+  This checks if all specified keys are present in the annex, 
+  and exits zero if so.
 
+  Exits 1 if the key is certainly not present in the annex.
+  Exits 100 if it's unable to tell (perhaps the key is in the process of
+  being removed from the annex).
+  
+  Used only by the gcrypt special remote.
+
 * recvkey directory key
 
   This runs rsync in server mode to receive the content of a key,
   and stores the content in the annex.
+  
+  Used only by the gcrypt special remote.
 
 * sendkey directory key
 
   This runs rsync in server mode to transfer out the content of a key.
-
-* transferinfo directory key
-
-  This is typically run at the same time as sendkey is sending a key
-  to the remote. Using it is optional, but is used to update
-  progress information for the transfer of the key.
-
-  It reads lines from standard input, each giving the number of bytes
-  that have been received so far. 
-
-* commit directory
-
-  This commits any staged changes to the git-annex branch.
-  It also runs the annex-content hook.
-
-* notifychanges directory
-
-  This is used by `git-annex remotedaemon` to be notified when
-  refs in the remote repository are changed.
-
-* gcryptsetup directory gcryptid
-
-  Sets up a repository as a gcrypt repository.
-
-* p2pstdio directory uuid
+  
+  Used only by the gcrypt special remote.
 
-  This causes git-annex-shell to communicate using the git-annex p2p
-  protocol over stdio. When supported by git-annex-shell, this allows
-  multiple actions to be run over a single connection, improving speed.
+* dropkey directory [key ...]
 
-  The uuid is the one belonging to the repository that will be
-  communicating with git-annex-shell.
+  This drops the annexed data for the specified keys.
+  
+  Used only by the gcrypt special remote.
 
 # OPTIONS
 
-Most options are the same as in git-annex. The ones specific
-to git-annex-shell are:
-
 * --uuid=UUID
 
   git-annex uses this to specify the UUID of the repository it was expecting
@@ -117,8 +96,7 @@
   past versions of git-annex-shell (that ignore these, but would choke
   on new dashed options).
 
-  Currently used fields include remoteuuid=, associatedfile=,
-  unlocked=, direct=, and autoinit=
+  Currently used fields are autoinit= and remoteuuid=
 
 # HOOK
 
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: 8.20211011
+Version: 8.20211028
 Cabal-Version: >= 1.10
 License: AGPL-3
 Maintainer: Joey Hess <id@joeyh.name>
@@ -728,7 +728,6 @@
     Command.Benchmark
     Command.CalcKey
     Command.CheckPresentKey
-    Command.Commit
     Command.Config
     Command.ConfigList
     Command.ContentLocation
@@ -768,7 +767,6 @@
     Command.Inprogress
     Command.List
     Command.Lock
-    Command.LockContent
     Command.Log
     Command.LookupKey
     Command.Map
@@ -809,7 +807,6 @@
     Command.Sync
     Command.Test
     Command.TestRemote
-    Command.TransferInfo
     Command.Transferrer
     Command.TransferKey
     Command.TransferKeys
diff --git a/stack.yaml b/stack.yaml
--- a/stack.yaml
+++ b/stack.yaml
@@ -14,17 +14,19 @@
     httpclientrestricted: true
 packages:
 - '.'
+resolver: lts-18.13
 extra-deps:
 - IfElse-0.85
 - aws-0.22
 - bloomfilter-2.0.1.0
-- filepath-bytestring-1.4.2.1.6
-- git-lfs-1.1.0
-- http-client-restricted-0.0.3
+- git-lfs-1.1.2
+- http-client-restricted-0.0.4
 - network-multicast-0.3.2
 - sandi-0.5
 - torrent-10000.1.1
+- base16-bytestring-0.1.1.7
+- base64-bytestring-1.0.0.3
 - bencode-0.6.1.1
+- http-client-0.7.9
 explicit-setup-deps:
   git-annex: true
-resolver: lts-16.27
