diff --git a/Annex.hs b/Annex.hs
--- a/Annex.hs
+++ b/Annex.hs
@@ -81,6 +81,7 @@
 import Utility.Url
 import Utility.ResourcePool
 import Utility.HumanTime
+import Git.Credential (CredentialCache(..))
 
 import "mtl" Control.Monad.Reader
 import Control.Concurrent
@@ -129,6 +130,7 @@
 	, forcebackend :: Maybe String
 	, useragent :: Maybe String
 	, desktopnotify :: DesktopNotify
+	, gitcredentialcache :: TMVar CredentialCache
 	}
 
 newAnnexRead :: GitConfig -> IO AnnexRead
@@ -140,6 +142,7 @@
 	si <- newTVarIO M.empty
 	tp <- newTransferrerPool
 	cm <- newTMVarIO M.empty
+	cc <- newTMVarIO (CredentialCache M.empty)
 	return $ AnnexRead
 		{ activekeys = emptyactivekeys
 		, activeremotes = emptyactiveremotes
@@ -157,6 +160,7 @@
 		, forcemincopies = Nothing
 		, useragent = Nothing
 		, desktopnotify = mempty
+		, gitcredentialcache = cc
 		}
 
 -- Values that can change while running an Annex action.
@@ -196,7 +200,7 @@
 	, cleanupactions :: M.Map CleanupAction (Annex ())
 	, sentinalstatus :: Maybe SentinalStatus
 	, errcounter :: Integer
-	, skippedfiles :: Bool
+	, reachedlimit :: Bool
 	, adjustedbranchrefreshcounter :: Integer
 	, unusedkeys :: Maybe (S.Set Key)
 	, tempurls :: M.Map Key URLString
@@ -249,7 +253,7 @@
 		, cleanupactions = M.empty
 		, sentinalstatus = Nothing
 		, errcounter = 0
-		, skippedfiles = False
+		, reachedlimit = False
 		, adjustedbranchrefreshcounter = 0
 		, unusedkeys = Nothing
 		, tempurls = M.empty
diff --git a/Annex/FileMatcher.hs b/Annex/FileMatcher.hs
--- a/Annex/FileMatcher.hs
+++ b/Annex/FileMatcher.hs
@@ -180,7 +180,7 @@
 	, ValueToken "copies" (usev limitCopies)
 	, ValueToken "lackingcopies" (usev $ limitLackingCopies False)
 	, ValueToken "approxlackingcopies" (usev $ limitLackingCopies True)
-	, ValueToken "inbacked" (usev limitInBackend)
+	, ValueToken "inbackend" (usev limitInBackend)
 	, ValueToken "metadata" (usev limitMetaData)
 	, ValueToken "inallgroup" (usev $ limitInAllGroup $ getGroupMap pcd)
 	] ++ commonKeyedTokens
diff --git a/Annex/Import.hs b/Annex/Import.hs
--- a/Annex/Import.hs
+++ b/Annex/Import.hs
@@ -600,7 +600,7 @@
 			let af = AssociatedFile (Just f)
 			let downloader p' tmpfile = do
 				_ <- Remote.retrieveExportWithContentIdentifier
-					ia loc cid (fromRawFilePath tmpfile)
+					ia loc [cid] (fromRawFilePath tmpfile)
 					(Left k)
 					(combineMeterUpdate p' p)
 				ok <- moveAnnex k af tmpfile
@@ -618,7 +618,7 @@
 	doimportsmall cidmap db loc cid sz p = do
 		let downloader tmpfile = do
 			(k, _) <- Remote.retrieveExportWithContentIdentifier
-				ia loc cid (fromRawFilePath tmpfile)
+				ia loc [cid] (fromRawFilePath tmpfile)
 				(Right (mkkey tmpfile))
 				p
 			case keyGitSha k of
@@ -641,7 +641,7 @@
 		let af = AssociatedFile (Just f)
 		let downloader tmpfile p = do
 			(k, _) <- Remote.retrieveExportWithContentIdentifier
-				ia loc cid (fromRawFilePath tmpfile)
+				ia loc [cid] (fromRawFilePath tmpfile)
 				(Right (mkkey tmpfile))
 				p
 			case keyGitSha k of
diff --git a/Annex/Init.hs b/Annex/Init.hs
--- a/Annex/Init.hs
+++ b/Annex/Init.hs
@@ -9,6 +9,7 @@
 {-# LANGUAGE OverloadedStrings #-}
 
 module Annex.Init (
+	checkInitializeAllowed,
 	ensureInitialized,
 	autoInitialize,
 	isInitialized,
@@ -68,11 +69,13 @@
 import Control.Concurrent.Async
 #endif
 
-checkInitializeAllowed :: Annex a -> Annex a
+data InitializeAllowed = InitializeAllowed
+
+checkInitializeAllowed :: (InitializeAllowed -> Annex a) -> Annex a
 checkInitializeAllowed a = guardSafeToUseRepo $ noAnnexFileContent' >>= \case
 	Nothing -> do
 		checkSqliteWorks
-		a
+		a InitializeAllowed
 	Just noannexmsg -> do
 		warning "Initialization prevented by .noannex file (remove the file to override)"
 		unless (null noannexmsg) $
@@ -100,7 +103,7 @@
 		Left _ -> [hostname, ":", reldir]
 
 initialize :: Bool -> Maybe String -> Maybe RepoVersion -> Annex ()
-initialize autoinit mdescription mversion = checkInitializeAllowed $ do
+initialize autoinit mdescription mversion = checkInitializeAllowed $ \initallowed -> do
 	{- Has to come before any commits are made as the shared
 	 - clone heuristic expects no local objects. -}
 	sharedclone <- checkSharedClone
@@ -110,7 +113,7 @@
 	ensureCommit $ Annex.Branch.create
 
 	prepUUID
-	initialize' autoinit mversion
+	initialize' autoinit mversion initallowed
 	
 	initSharedClone sharedclone
 	
@@ -122,8 +125,8 @@
 
 -- Everything except for uuid setup, shared clone setup, and initial
 -- description.
-initialize' :: Bool -> Maybe RepoVersion -> Annex ()
-initialize' autoinit mversion = checkInitializeAllowed $ do
+initialize' :: Bool -> Maybe RepoVersion -> InitializeAllowed -> Annex ()
+initialize' autoinit mversion _initallowed = do
 	checkLockSupport
 	checkFifoSupport
 	checkCrippledFileSystem
@@ -198,8 +201,8 @@
  -
  - Checks repository version and handles upgrades too.
  -}
-ensureInitialized :: Annex ()
-ensureInitialized = getInitializedVersion >>= maybe needsinit checkUpgrade
+ensureInitialized :: Annex [Remote] -> Annex ()
+ensureInitialized remotelist = getInitializedVersion >>= maybe needsinit checkUpgrade
   where
 	needsinit = ifM autoInitializeAllowed
 		( do
@@ -207,7 +210,7 @@
 				Right () -> noop
 				Left e -> giveup $ show e ++ "\n" ++
 					"git-annex: automatic initialization failed due to above problems"
-			autoEnableSpecialRemotes
+			autoEnableSpecialRemotes remotelist
 		, giveup "First run: git-annex init"
 		)
 
@@ -251,13 +254,13 @@
  -
  - Checks repository version and handles upgrades too.
  -}
-autoInitialize :: Annex ()
-autoInitialize = getInitializedVersion >>= maybe needsinit checkUpgrade
+autoInitialize :: Annex [Remote] -> Annex ()
+autoInitialize remotelist = getInitializedVersion >>= maybe needsinit checkUpgrade
   where
 	needsinit =
 		whenM (initializeAllowed <&&> autoInitializeAllowed) $ do
 			initialize True Nothing Nothing
-			autoEnableSpecialRemotes
+			autoEnableSpecialRemotes remotelist
 
 {- Checks if a repository is initialized. Does not check version for ugrade. -}
 isInitialized :: Annex Bool
@@ -437,9 +440,17 @@
 {- Try to enable any special remotes that are configured to do so.
  - 
  - The enabling is done in a child process to avoid it using stdio.
+ -
+ - The remotelist should be Remote.List.remoteList, which cannot
+ - be imported here due to a dependency loop.
  -}
-autoEnableSpecialRemotes :: Annex ()
-autoEnableSpecialRemotes = do
+autoEnableSpecialRemotes :: Annex [Remote] -> Annex ()
+autoEnableSpecialRemotes remotelist = do
+	-- Get all existing git remotes to probe for their uuid here,
+	-- so it is not done inside the child process. Doing it in there
+	-- could result in password prompts for http credentials,
+	-- which would then not end up cached in this process's state.
+	_ <- remotelist
 	rp <- fromRawFilePath <$> fromRepo Git.repoPath
 	withNullHandle $ \nullh -> gitAnnexChildProcess "init"
 		[ Param "--autoenable" ]
diff --git a/Annex/Link.hs b/Annex/Link.hs
--- a/Annex/Link.hs
+++ b/Annex/Link.hs
@@ -25,6 +25,7 @@
 import qualified Git.LockFile
 import qualified Git.Env
 import qualified Git
+import Logs.Restage
 import Git.Types
 import Git.FilePath
 import Git.Config
@@ -35,7 +36,6 @@
 import Utility.InodeCache
 import Utility.Tmp.Dir
 import Utility.CopyFile
-import Utility.Tuple
 import qualified Database.Keys.Handle
 import qualified Utility.RawFilePath as R
 
@@ -155,6 +155,10 @@
  - when content is added/removed, to prevent git status from showing
  - it as modified.
  -
+ - The InodeCache is for the worktree file. It is used to detect when
+ - the worktree file is changed by something else before git update-index
+ - gets to look at it.
+ -
  - Asks git to refresh its index information for the file.
  - That in turn runs the clean filter on the file; when the clean
  - filter produces the same pointer that was in the index before, git
@@ -165,92 +169,120 @@
  - that. So it's safe to call at any time and any situation.
  -
  - If the index is known to be locked (eg, git add has run git-annex),
- - that would fail. Restage False will prevent the index being updated.
- - Will display a message to help the user understand why
- - the file will appear to be modified.
+ - that would fail. Restage False will prevent the index being updated,
+ - and will store it in the restage log. Displays a message to help the
+ - user understand why the file will appear to be modified.
  -
  - This uses the git queue, so the update is not performed immediately,
- - and this can be run multiple times cheaply.
- -
- - The InodeCache is for the worktree file. It is used to detect when
- - the worktree file is changed by something else before git update-index
- - gets to look at it.
+ - and this can be run multiple times cheaply. Using the git queue also
+ - prevents building up too large a number of updates when many files
+ - are being processed. It's also recorded in the restage log so that,
+ - if the process is interrupted before the git queue is fulushed, the
+ - restage will be taken care of later.
  -}
 restagePointerFile :: Restage -> RawFilePath -> InodeCache -> Annex ()
-restagePointerFile (Restage False) f _ =
+restagePointerFile (Restage False) f orig = do
+	flip writeRestageLog orig =<< inRepo (toTopFilePath f)
 	toplevelWarning True $ unableToRestage $ Just $ fromRawFilePath f
-restagePointerFile (Restage True) f orig = withTSDelta $ \tsd ->
+restagePointerFile (Restage True) f orig = do
+	flip writeRestageLog orig =<< inRepo (toTopFilePath f)
 	-- Avoid refreshing the index if run by the
 	-- smudge clean filter, because git uses that when
 	-- it's already refreshing the index, probably because
 	-- this very action is running. Running it again would likely
 	-- deadlock.
-	unlessM (Annex.getState Annex.insmudgecleanfilter) $ do
-		-- update-index is documented as picky about "./file" and it
-		-- fails on "../../repo/path/file" when cwd is not in the repo 
-		-- being acted on. Avoid these problems with an absolute path.
-		absf <- liftIO $ absPath f
-		Annex.Queue.addFlushAction runner [(absf, isunmodified tsd, inodeCacheFileSize orig)]
-  where
-	isunmodified tsd = genInodeCache f tsd >>= return . \case
-		Nothing -> False
-		Just new -> compareStrong orig new
+	unlessM (Annex.getState Annex.insmudgecleanfilter) $
+		Annex.Queue.addFlushAction restagePointerFileRunner [f]
 
-	-- Other changes to the files may have been staged before this
-	-- gets a chance to run. To avoid a race with any staging of
-	-- changes, first lock the index file. Then run git update-index
-	-- on all still-unmodified files, using a copy of the index file,
-	-- to bypass the lock. Then replace the old index file with the new
-	-- updated index file.
-	runner :: Git.Queue.FlushActionRunner Annex
-	runner = Git.Queue.FlushActionRunner "restagePointerFile" $ \r l -> do
-		-- 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)
-		    lockindex = liftIO $ catchMaybeIO $ Git.LockFile.openLock' lock
-		    unlockindex = liftIO . maybe noop Git.LockFile.closeLock
-		    showwarning = warning $ unableToRestage Nothing
-		    go Nothing = showwarning
-		    go (Just _) = withTmpDirIn (fromRawFilePath $ Git.localGitDir r) "annexindex" $ \tmpdir -> do
-			let tmpindex = toRawFilePath (tmpdir </> "index")
-			let updatetmpindex = do
-				r' <- liftIO $ Git.Env.addGitEnv r Git.Index.indexEnv
-					=<< Git.Index.indexEnvVal tmpindex
-				-- Avoid git warning about CRLF munging.
-				let r'' = r' { gitGlobalOpts = gitGlobalOpts r' ++
-					[ Param "-c"
-					, Param $ "core.safecrlf=" ++ boolConfig False
-					] }
-				configfilterprocess l $ runsGitAnnexChildProcessViaGit' r'' $ \r''' ->
-					liftIO $ Git.UpdateIndex.refreshIndex r''' $ \feed ->
-						forM_ l $ \(f', checkunmodified, _) ->
-							whenM checkunmodified $
-								feed f'
-			let replaceindex = catchBoolIO $ do
-				moveFile tmpindex realindex
-				return True
-			ok <- liftIO (createLinkOrCopy realindex tmpindex)
-				<&&> updatetmpindex
-				<&&> liftIO replaceindex
-			unless ok showwarning
+restagePointerFileRunner :: Git.Queue.FlushActionRunner Annex
+restagePointerFileRunner = 
+	Git.Queue.FlushActionRunner "restagePointerFiles" $ \r _fs ->
+		restagePointerFiles r
+
+-- Restage all files in the restage log that have not been modified.
+--
+-- Other changes to the files may have been staged before this
+-- gets a chance to run. To avoid a race with any staging of
+-- changes, first lock the index file. Then run git update-index
+-- on all still-unmodified files, using a copy of the index file,
+-- to bypass the lock. Then replace the old index file with the new
+-- updated index file.
+restagePointerFiles :: Git.Repo -> Annex ()
+restagePointerFiles r = unlessM (Annex.getState Annex.insmudgecleanfilter) $ do
+	-- 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
+	numsz@(numfiles, _) <- calcnumsz
+	let lock = fromRawFilePath (Git.Index.indexFileLock realindex)
+	    lockindex = liftIO $ catchMaybeIO $ Git.LockFile.openLock' lock
+	    unlockindex = liftIO . maybe noop Git.LockFile.closeLock
+	    showwarning = warning $ unableToRestage Nothing
+	    go Nothing = showwarning
+	    go (Just _) = withtmpdir $ \tmpdir -> do
+		tsd <- getTSDelta 
+		let tmpindex = toRawFilePath (tmpdir </> "index")
+		let replaceindex = liftIO $ moveFile tmpindex realindex
+		let updatetmpindex = do
+			r' <- liftIO $ Git.Env.addGitEnv r Git.Index.indexEnv
+				=<< Git.Index.indexEnvVal tmpindex
+			configfilterprocess numsz $
+				runupdateindex tsd r' replaceindex
+			return True
+		ok <- liftIO (createLinkOrCopy realindex tmpindex)
+			<&&> catchBoolIO updatetmpindex
+		unless ok showwarning
+	when (numfiles > 0) $
 		bracket lockindex unlockindex go
+  where
+	withtmpdir = withTmpDirIn (fromRawFilePath $ Git.localGitDir r) "annexindex"
+
+	isunmodified tsd f orig = 
+		genInodeCache f tsd >>= return . \case
+			Nothing -> False
+			Just new -> compareStrong orig new
+			
+	{- Avoid git warning about CRLF munging -}
+	avoidcrlfwarning r' = r' { gitGlobalOpts = gitGlobalOpts r' ++
+		[ Param "-c"
+		, Param $ "core.safecrlf=" ++ boolConfig False
+		] }
+			
+	runupdateindex tsd r' replaceindex = 
+		runsGitAnnexChildProcessViaGit' (avoidcrlfwarning r') $ \r'' ->
+			Git.UpdateIndex.refreshIndex r'' $ \feeder -> do
+				let atend = do
+					-- wait for index write
+					liftIO $ feeder Nothing
+					replaceindex
+				streamRestageLog atend $ \topf ic -> do
+					let f = fromTopFilePath topf r''
+					liftIO $ whenM (isunmodified tsd f ic) $
+						feedupdateindex f feeder
 	
+	{- update-index is documented as picky about "./file" and it
+	 - fails on "../../repo/path/file" when cwd is not in the repo 
+	 - being acted on. Avoid these problems with an absolute path.
+	 -}
+	feedupdateindex f feeder = do
+		absf <- absPath f
+		feeder (Just absf)
+	
+	calcnumsz = calcRestageLog (0, 0) $ \(_f, ic) (numfiles, sizefiles) ->
+		(numfiles+1, sizefiles + inodeCacheFileSize ic)
+
 	{- filter.annex.process configured to use git-annex filter-process
 	 - is sometimes faster and sometimes slower than using
 	 - git-annex smudge. The latter is run once per file, while
 	 - the former has the content of files piped to it.
 	 -}
-	filterprocessfaster l = 
-		let numfiles = genericLength l
-		    sizefiles = sum (map thd3 l)
-		    -- estimates based on benchmarking
-		    estimate_enabled = sizefiles `div` 191739611
+	filterprocessfaster :: (Integer, FileSize) -> Bool
+	filterprocessfaster (numfiles, sizefiles) = 
+		let estimate_enabled = sizefiles `div` 191739611
 		    estimate_disabled = numfiles `div` 7
 		in estimate_enabled <= estimate_disabled
 	 
@@ -262,10 +294,10 @@
 	  - case this process is terminated early, the next time this
 	  - runs it will take care of reversing the modification.
 	  -}
-	configfilterprocess l = bracket setup cleanup . const
+	configfilterprocess numsz = bracket setup cleanup . const
 	  where
 		setup
-			| filterprocessfaster l = return Nothing
+			| filterprocessfaster numsz = return Nothing
 			| otherwise = fromRepo (Git.Config.getMaybe ck) >>= \case
 				Nothing -> return Nothing
 				Just v -> do
@@ -292,7 +324,7 @@
 	, "This is only a cosmetic problem affecting git status; git add,"
 	, "git commit, etc won't be affected."
 	, "To fix the git status display, you can run:"
-	, "git update-index -q --refresh " ++ fromMaybe "<file>" mf
+	, "git-annex restage"
 	]
 
 {- Parses a symlink target or a pointer file to a Key.
diff --git a/Annex/Locations.hs b/Annex/Locations.hs
--- a/Annex/Locations.hs
+++ b/Annex/Locations.hs
@@ -49,6 +49,8 @@
 	gitAnnexUpgradeLock,
 	gitAnnexSmudgeLog,
 	gitAnnexSmudgeLock,
+	gitAnnexRestageLog,
+	gitAnnexRestageLock,
 	gitAnnexMoveLog,
 	gitAnnexMoveLock,
 	gitAnnexExportDir,
@@ -370,13 +372,21 @@
 gitAnnexUpgradeLock :: Git.Repo -> RawFilePath
 gitAnnexUpgradeLock r = gitAnnexDir r P.</> "upgrade.lck"
 
-{- .git/annex/smudge.log is used to log smudges worktree files that need to
+{- .git/annex/smudge.log is used to log smudged worktree files that need to
  - be updated. -}
 gitAnnexSmudgeLog :: Git.Repo -> RawFilePath
 gitAnnexSmudgeLog r = gitAnnexDir r P.</> "smudge.log"
 
 gitAnnexSmudgeLock :: Git.Repo -> RawFilePath
 gitAnnexSmudgeLock r = gitAnnexDir r P.</> "smudge.lck"
+
+{- .git/annex/restage.log is used to log worktree files that need to be
+ - restaged in git -}
+gitAnnexRestageLog :: Git.Repo -> RawFilePath
+gitAnnexRestageLog r = gitAnnexDir r P.</> "restage.log"
+
+gitAnnexRestageLock :: Git.Repo -> RawFilePath
+gitAnnexRestageLock r = gitAnnexDir r P.</> "restage.lck"
 
 {- .git/annex/move.log is used to log moves that are in progress,
  - to better support resuming an interrupted move. -}
diff --git a/Annex/Perms.hs b/Annex/Perms.hs
--- a/Annex/Perms.hs
+++ b/Annex/Perms.hs
@@ -154,7 +154,7 @@
  - that happens with write permissions.
  -}
 freezeContent :: RawFilePath -> Annex ()
-freezeContent file = unlessM crippledFileSystem $
+freezeContent file =
 	withShared $ \sr -> freezeContent' sr file
 
 freezeContent' :: SharedRepository -> RawFilePath -> Annex ()
@@ -163,7 +163,7 @@
 freezeContent'' :: SharedRepository -> RawFilePath -> Maybe RepoVersion -> Annex ()
 freezeContent'' sr file rv = do
 	fastDebug "Annex.Perms" ("freezing content " ++ fromRawFilePath file)
-	go sr
+	unlessM crippledFileSystem $ go sr
 	freezeHook file
   where
 	go GroupShared = if versionNeedsWritableContentFiles rv
@@ -253,7 +253,7 @@
  - permissions. -}
 thawPerms :: Annex () -> Annex () -> Annex ()
 thawPerms a hook = ifM crippledFileSystem
-	( void (tryNonAsync a)
+	( hook >> void (tryNonAsync a)
 	, hook >> a
 	)
 
@@ -263,9 +263,9 @@
  - file.
  -}
 freezeContentDir :: RawFilePath -> Annex ()
-freezeContentDir file = unlessM crippledFileSystem $ do
+freezeContentDir file = do
 	fastDebug "Annex.Perms" ("freezing content directory " ++ fromRawFilePath dir)
-	withShared go
+	unlessM crippledFileSystem $ withShared go
 	freezeHook dir
   where
 	dir = parentDir file
@@ -287,9 +287,8 @@
 	unlessM (liftIO $ R.doesPathExist dir) $
 		createAnnexDirectory dir 
 	-- might have already existed with restricted perms
-	unlessM crippledFileSystem $ do
-		thawHook dir
-		liftIO $ allowWrite dir
+	thawHook dir
+	unlessM crippledFileSystem $ liftIO $ allowWrite dir
   where
 	dir = parentDir dest
 
diff --git a/Annex/PidLock.hs b/Annex/PidLock.hs
--- a/Annex/PidLock.hs
+++ b/Annex/PidLock.hs
@@ -106,11 +106,15 @@
 runsGitAnnexChildProcessViaGit a = a
 #endif
 
-runsGitAnnexChildProcessViaGit' :: Git.Repo -> (Git.Repo -> IO a) -> Annex a
+{- Like runsGitAnnexChildProcessViaGit, but the Annex state is not
+ - modified. Instead the input Repo's state is modified to set the 
+ - necessary env var when git is run in that Repo.
+ -}
+runsGitAnnexChildProcessViaGit' :: Git.Repo -> (Git.Repo -> Annex a) -> Annex a
 #ifndef mingw32_HOST_OS
 runsGitAnnexChildProcessViaGit' r a = pidLockFile >>= \case
-	Nothing -> liftIO $ a r
-	Just pidlock -> liftIO $ bracket (setup pidlock) cleanup (go pidlock)
+	Nothing -> a r
+	Just pidlock -> bracketIO (setup pidlock) cleanup (go pidlock)
   where
 	setup pidlock = fmap fst <$> PidP.tryLock' pidlock
 	
@@ -119,9 +123,9 @@
 	
 	go _ Nothing = a r
 	go pidlock (Just _h) = do
-		v <- PidF.pidLockEnv pidlock
-		r' <- addGitEnv r v PidF.pidLockEnvValue
+		v <- liftIO $ PidF.pidLockEnv pidlock
+		r' <- liftIO $ addGitEnv r v PidF.pidLockEnvValue
 		a r'
 #else
-runsGitAnnexChildProcessViaGit' r a = liftIO $ a r
+runsGitAnnexChildProcessViaGit' r a = a r
 #endif
diff --git a/Annex/Queue.hs b/Annex/Queue.hs
--- a/Annex/Queue.hs
+++ b/Annex/Queue.hs
@@ -31,7 +31,7 @@
 	store =<< flushWhenFull =<<
 		(Git.Queue.addCommand commonparams command params files q =<< gitRepo)
 
-addFlushAction :: Git.Queue.FlushActionRunner Annex -> [(RawFilePath, IO Bool, FileSize)] -> Annex ()
+addFlushAction :: Git.Queue.FlushActionRunner Annex -> [RawFilePath] -> Annex ()
 addFlushAction runner files = do
 	q <- get
 	store =<< flushWhenFull =<<
diff --git a/Annex/TransferrerPool.hs b/Annex/TransferrerPool.hs
--- a/Annex/TransferrerPool.hs
+++ b/Annex/TransferrerPool.hs
@@ -1,6 +1,6 @@
 {- A pool of "git-annex transferrer" processes
  -
- - Copyright 2013-2021 Joey Hess <id@joeyh.name>
+ - Copyright 2013-2022 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -22,6 +22,7 @@
 import Messages.Serialized
 import Annex.Path
 import Annex.StallDetection
+import Annex.Link
 import Utility.Batch
 import Utility.Metered
 import qualified Utility.SimpleProtocol as Proto
@@ -217,9 +218,15 @@
 		, transferrerWrite = writeh
 		, transferrerHandle = ph
 		, transferrerShutdown = do
-			hClose readh
+			-- The transferrer may write to stdout
+			-- as it's shutting down, so don't close
+			-- the readh right away. Instead, drain
+			-- anything sent to it.
+			drainer <- async $ void $ hGetContents readh
 			hClose writeh
 			void $ waitForProcess ph
+			wait drainer
+			hClose readh
 			unregistersignalprop
 		}
 
@@ -280,3 +287,10 @@
 	liftIO $ forM_ pool $ \case
 		TransferrerPoolItem (Just t) _ -> transferrerShutdown t
 		TransferrerPoolItem Nothing _ -> noop
+	-- Transferrers usually restage pointer files themselves,
+	-- but when killTransferrer is used, a transferrer may have
+	-- pointer files it has not gotten around to restaging yet.
+	-- So, restage pointer files here in clean up from such killed
+	-- transferrers.
+	unless (null pool) $
+		restagePointerFiles =<< Annex.gitRepo
diff --git a/Annex/Url.hs b/Annex/Url.hs
--- a/Annex/Url.hs
+++ b/Annex/Url.hs
@@ -147,20 +147,19 @@
 
 -- When downloading an url, if authentication is needed, uses
 -- git-credential to prompt for username and password.
+--
+-- Note that, when the downloader is curl, it will not use git-credential.
+-- If the user wants to, they can configure curl to use a netrc file that
+-- handles authentication.
 withUrlOptionsPromptingCreds :: (U.UrlOptions -> Annex a) -> Annex a
 withUrlOptionsPromptingCreds a = do
 	g <- Annex.gitRepo
 	uo <- getUrlOptions
 	prompter <- mkPrompter
+	cc <- Annex.getRead Annex.gitcredentialcache
 	a $ uo
 		{ U.getBasicAuth = \u -> prompter $
-			getBasicAuthFromCredential g u
-		-- Can't download with curl and handle basic auth,
-		-- so make sure it uses conduit.
-		, U.urlDownloader = case U.urlDownloader uo of
-			U.DownloadWithCurl _ -> U.DownloadWithConduit $
-				U.DownloadWithCurlRestricted mempty
-			v -> v
+			getBasicAuthFromCredential g cc u
 		}
 
 checkBoth :: U.URLString -> Maybe Integer -> U.UrlOptions -> Annex Bool
diff --git a/BuildFlags.hs b/BuildFlags.hs
--- a/BuildFlags.hs
+++ b/BuildFlags.hs
@@ -24,7 +24,7 @@
 #ifdef WITH_WEBAPP
 	, "Webapp"
 #else
-#warning Building without the webapp. You probably need to install Yesod..
+#warning Building without the webapp.
 #endif
 #ifdef WITH_PAIRING
 	, "Pairing"
@@ -51,6 +51,9 @@
 #endif
 #ifdef WITH_MAGICMIME
 	, "MagicMime"
+#endif
+#ifdef WITH_BENCHMARK
+	, "Benchmark"
 #endif
 #ifdef DEBUGLOCKS
 	, "DebugLocks"
diff --git a/CHANGELOG b/CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,52 @@
+git-annex (10.20220927) upstream; urgency=medium
+
+  * Fix a bug in the last release that caused v8 repositories to upgrade
+    immediately to v10, rather than taking the scheduled 1 year to do so.
+  * annex.diskreserve default increased from 1 mb to 100 mb.
+  * Include the assistant and webapp when building with cabal 3.4.1.0.
+  * Merged the webapp build flag into the assistant build flag.
+  * Optimise linker in linux standalone tarballs.
+  * Fix crash importing from a directory special remote that contains
+    a broken symlink.
+  * When accessing a git remote over http needs a git credential
+    prompt for a password, cache it for the lifetime of the git-annex
+    process, rather than repeatedly prompting.
+  * Use curl for downloads from git remotes when annex.url-options is set.
+  * Fix a reversion that made dead keys not be skipped when operating on
+    all keys via --all or in a bare repo.
+    (Introduced in version 8.20200720)
+  * vicfg: Include mincopies configuration.
+  * Improve handling of directory special remotes with importtree=yes whose
+    ignoreinode setting has been changed. When getting a file from such a
+    remote, accept the content that would have been accepted with the
+    previous ignoreinode setting.
+  * directory, adb: Fixed a bug with importtree=yes and multiple files
+    in the special remote have the same content, that caused it to
+    refuse to get a file from the special remote, incorrectly complaining
+    that it had changed, due to only accepting the inode+mtime of one file
+    (that was since modified or deleted) and not accepting the inode+mtime
+    of other duplicate files.
+  * Fix a reversion that prevented git-annex from working in a
+    repository when --git-dir or GIT_DIR is specified to relocate the git
+    directory to somewhere else.
+    (Introduced in version 10.20220525)
+  * Improved handling of --time-limit when combined with -J
+  * Fix updating git index file after getting an unlocked file 
+    when annex.stalldetection is set.
+  * restage: New git-annex command, handles restaging unlocked files.
+  * test: Added --test-with-git-config option.
+  * Run annex.freezecontent-command and annex.thawcontent-command
+    when on a crippled filesystem.
+    Thanks, Reiko Asakura
+  * enable-tor: Fix breakage caused by git's fix for CVE-2022-24765.
+  * Let GIT_DIR and --git-dir override git's protection against operating
+    in a repository owned by another user.
+  * p2p: Pass wormhole the --appid option before the receive/send command,
+    as it does not accept that option after the command
+  * Support "inbackend" in preferred content expressions.
+
+ -- Joey Hess <id@joeyh.name>  Tue, 27 Sep 2022 13:31:18 -0400
+
 git-annex (10.20220822) upstream; urgency=medium
 
   * v8 repositories now automatically upgrade to v9, which will in turn
diff --git a/CmdLine/Action.hs b/CmdLine/Action.hs
--- a/CmdLine/Action.hs
+++ b/CmdLine/Action.hs
@@ -32,8 +32,8 @@
 {- Runs a command, starting with the check stage, and then
  - the seek stage. Finishes by running the continuation.
  -
- - Can exit when there was a problem or when files were skipped.
- - Also shows a count of any failures when that is enabled.
+ - Can exit when there was a problem or when a time or size limit was
+ - reached. Also shows a count of any failures when that is enabled.
  -}
 performCommandAction :: Bool -> Command -> CommandSeek -> Annex () -> Annex ()
 performCommandAction canexit (Command { cmdcheck = c, cmdname = name }) seek cont = do
@@ -43,19 +43,19 @@
 	finishCommandActions
 	cont
 	st <- Annex.getState id
-	when canexit $ liftIO $ case (Annex.errcounter st, Annex.skippedfiles st) of
+	when canexit $ liftIO $ case (Annex.errcounter st, Annex.reachedlimit st) of
 		(0, False) -> noop
 		(errcnt, False) -> do
 			showerrcount errcnt
 			exitWith $ ExitFailure 1
-		(0, True) -> exitskipped
+		(0, True) -> exitreachedlimit
 		(errcnt, True) -> do
 			showerrcount errcnt
-			exitskipped
+			exitreachedlimit
   where
 	showerrcount cnt = hPutStrLn stderr $
 		name ++ ": " ++ show cnt ++ " failed"
-	exitskipped = exitWith $ ExitFailure 101
+	exitreachedlimit = exitWith $ ExitFailure 101
 
 commandActions :: [CommandStart] -> Annex ()
 commandActions = mapM_ commandAction
@@ -328,7 +328,7 @@
 			Nothing -> do
 				fsz <- catchMaybeIO $ withObjectLoc k $
 					liftIO . getFileSize
-				maybe skipped go fsz
+				maybe reachedlimit go fsz
 		Nothing -> a
   where
 	go sz = do
@@ -342,6 +342,6 @@
 				else return False
 		if fits 
 			then a
-			else skipped
+			else reachedlimit
 	
-	skipped = Annex.changeState $ \s -> s { Annex.skippedfiles = True }
+	reachedlimit = Annex.changeState $ \s -> s { Annex.reachedlimit = True }
diff --git a/CmdLine/GitAnnex.hs b/CmdLine/GitAnnex.hs
--- a/CmdLine/GitAnnex.hs
+++ b/CmdLine/GitAnnex.hs
@@ -1,6 +1,6 @@
 {- git-annex main program
  -
- - Copyright 2010-2021 Joey Hess <id@joeyh.name>
+ - Copyright 2010-2022 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -114,6 +114,7 @@
 import qualified Command.DiffDriver
 import qualified Command.Smudge
 import qualified Command.FilterProcess
+import qualified Command.Restage
 import qualified Command.Undo
 import qualified Command.Version
 import qualified Command.RemoteDaemon
@@ -228,6 +229,7 @@
 	, Command.DiffDriver.cmd
 	, Command.Smudge.cmd
 	, Command.FilterProcess.cmd
+	, Command.Restage.cmd
 	, Command.Undo.cmd
 	, Command.Version.cmd
 	, Command.RemoteDaemon.cmd
diff --git a/CmdLine/Seek.hs b/CmdLine/Seek.hs
--- a/CmdLine/Seek.hs
+++ b/CmdLine/Seek.hs
@@ -4,7 +4,7 @@
  - the values a user passes to a command, and prepare actions operating
  - on them.
  -
- - Copyright 2010-2021 Joey Hess <id@joeyh.name>
+ - Copyright 2010-2022 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -27,6 +27,7 @@
 import CmdLine.Action
 import Logs
 import Logs.Unused
+import Logs.Location
 import Types.Transfer
 import Logs.Transfer
 import Types.Link
@@ -40,7 +41,6 @@
 import Annex.InodeSentinal
 import Annex.Concurrent
 import Annex.CheckIgnore
-import Annex.Action
 import qualified Annex.Branch
 import qualified Database.Keys
 import qualified Utility.RawFilePath as R
@@ -48,6 +48,7 @@
 import Utility.HumanTime
 
 import Control.Concurrent.Async
+import Control.Concurrent.STM
 import System.Posix.Types
 import Data.IORef
 import Data.Time.Clock.POSIX
@@ -109,6 +110,10 @@
 			a f
 		go matcher checktimelimit ps fs		
 	
+	-- Using getFileStatus not getSymbolicLinkStatus because it should
+	-- fail if the path that the user provided is a broken symlink,
+	-- the same as it fails if the path that the user provided does not
+	-- exist.
 	get p = ifM (isDirectory <$> getFileStatus p)
 		( map (\f -> 
 			let f' = toRawFilePath f
@@ -279,7 +284,8 @@
 		let go reader = reader >>= \case
 			Just (k, f, content) -> checktimelimit (discard reader) $ do
 				maybe noop (Annex.Branch.precache f) content
-				keyaction Nothing (SeekInput [], k, mkActionItem k)
+				unlessM (checkDead k) $
+					keyaction Nothing (SeekInput [], k, mkActionItem k)
 				go reader
 			Nothing -> return ()
 		Annex.Branch.overBranchFileContents getk go >>= \case
@@ -604,20 +610,25 @@
 notSymlink f = liftIO $ not . isSymbolicLink <$> R.getSymbolicLinkStatus f
 
 {- Returns an action that, when there's a time limit, can be used
- - to check it before processing a file. The first action is run when over the
- - time limit, otherwise the second action is run. -}
+ - to check it before processing a file. The first action is run when
+ - over the time limit, otherwise the second action is run one time to
+ - clean up. -}
 mkCheckTimeLimit :: Annex (Annex () -> Annex () -> Annex ())
 mkCheckTimeLimit = Annex.getState Annex.timelimit >>= \case
 	Nothing -> return $ \_ a -> a
-	Just (duration, cutoff) -> return $ \cleanup a -> do
-		now <- liftIO getPOSIXTime
-		if now > cutoff
-			then do
-				warning $ "Time limit (" ++ fromDuration duration ++ ") reached! Shutting down..."
-				shutdown True
-				cleanup
-				liftIO $ exitWith $ ExitFailure 101
-			else a
+	Just (duration, cutoff) -> do
+		warningshownv <- liftIO $ newTVarIO False
+		return $ \cleanup a -> do
+			now <- liftIO getPOSIXTime
+			if now > cutoff
+				then do
+					warningshown <- liftIO $ atomically $
+						swapTVar warningshownv True
+					unless warningshown $ do
+						Annex.changeState $ \s -> s { Annex.reachedlimit = True }
+						warning $ "Time limit (" ++ fromDuration duration ++ ") reached! Shutting down..."
+						cleanup
+				else a
 
 propagateLsFilesError :: IO Bool -> Annex ()
 propagateLsFilesError cleanup =
diff --git a/Command.hs b/Command.hs
--- a/Command.hs
+++ b/Command.hs
@@ -28,6 +28,7 @@
 import Types.Transfer
 import Types.ActionItem
 import Types.WorkerPool as ReExported
+import Remote.List
 
 {- Generates a normal Command -}
 command :: String -> CommandSection -> String -> CmdParamsDesc -> (CmdParamsDesc -> CommandParser) -> Command
@@ -125,7 +126,7 @@
 commonChecks = [repoExists]
 
 repoExists :: CommandCheck
-repoExists = CommandCheck 0 ensureInitialized
+repoExists = CommandCheck 0 (ensureInitialized remoteList)
 
 notBareRepo :: Command -> Command
 notBareRepo = addCheck checkNotBareRepo
diff --git a/Command/Assistant.hs b/Command/Assistant.hs
--- a/Command/Assistant.hs
+++ b/Command/Assistant.hs
@@ -16,6 +16,7 @@
 import qualified BuildInfo
 import Utility.HumanTime
 import Assistant.Install
+import Remote.List
 
 import Control.Concurrent.Async
 
@@ -62,7 +63,7 @@
 		stop
 	| otherwise = do
 		liftIO ensureInstalled
-		ensureInitialized
+		ensureInitialized remoteList
 		Command.Watch.start True (daemonOptions o) (startDelayOption o)
 
 startNoRepo :: AssistantOptions -> IO ()
diff --git a/Command/EnableTor.hs b/Command/EnableTor.hs
--- a/Command/EnableTor.hs
+++ b/Command/EnableTor.hs
@@ -1,6 +1,6 @@
 {- git-annex command
  -
- - Copyright 2016 Joey Hess <id@joeyh.name>
+ - Copyright 2016-2022 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -22,6 +22,8 @@
 import qualified P2P.Protocol as P2P
 import Utility.ThreadScheduler
 import RemoteDaemon.Transport.Tor
+import Git.Types
+import Config
 
 import Control.Concurrent.Async
 import qualified Network.Socket as S
@@ -70,6 +72,15 @@
 #endif
   where
 	go userid = do
+		-- Usually git will refuse to read local configs of a git
+		-- repo belonging to another user. But in this case, the
+		-- user wants this command, run as root, to operate on
+		-- their repo. Behave as if --git-dir had been used to
+		-- specify that the git directory is intended to be used.
+		Annex.adjustGitRepo $ \r -> return $ r
+			{ gitDirSpecifiedExplicitly = True }
+		reloadConfig
+
 		uuid <- getUUID
 		when (uuid == NoUUID) $
 			giveup "This can only be run in a git-annex repository."
diff --git a/Command/Fsck.hs b/Command/Fsck.hs
--- a/Command/Fsck.hs
+++ b/Command/Fsck.hs
@@ -574,7 +574,7 @@
 	(deadlocations, safelocations) <- trustPartition DeadTrusted otherlocations
 	let present = length safelocations
 	if present < fromNumCopies numcopies
-		then ifM (pure (not hasafile) <&&> checkDead key)
+		then ifM (checkDead key)
 			( do
 				showLongNote $ "This key is dead, skipping."
 				return True
diff --git a/Command/Move.hs b/Command/Move.hs
--- a/Command/Move.hs
+++ b/Command/Move.hs
@@ -396,8 +396,7 @@
 	go logf
 		-- Only need to check log when there is a copy.
 		| deststartedwithcopy = do
-			lck <- fromRepo gitAnnexMoveLock
-			wasnocopy <- checkLogFile (fromRawFilePath logf) lck
+			wasnocopy <- checkLogFile (fromRawFilePath logf)
 				(== logline)
 			if wasnocopy
 				then go' logf False
diff --git a/Command/Reinit.hs b/Command/Reinit.hs
--- a/Command/Reinit.hs
+++ b/Command/Reinit.hs
@@ -35,6 +35,6 @@
 		then return $ toUUID s
 		else Remote.nameToUUID s
 	storeUUID u
-	initialize' False Nothing
+	checkInitializeAllowed $ initialize' False Nothing
 	Annex.SpecialRemote.autoEnable
 	next $ return True
diff --git a/Command/Restage.hs b/Command/Restage.hs
new file mode 100644
--- /dev/null
+++ b/Command/Restage.hs
@@ -0,0 +1,25 @@
+{- git-annex command
+ -
+ - Copyright 2022 Joey Hess <id@joeyh.name>
+ -
+ - Licensed under the GNU AGPL version 3 or higher.
+ -}
+
+module Command.Restage where
+
+import Command
+import qualified Annex
+import Annex.Link
+
+cmd :: Command
+cmd = command "restage" SectionPlumbing 
+	"estages unlocked files in the git index"
+	paramNothing (withParams seek)
+
+seek :: CmdParams -> CommandSeek
+seek = withNothing (commandAction start)
+
+start :: CommandStart
+start = starting "restage" (ActionItemOther Nothing) (SeekInput []) $ do
+	restagePointerFiles =<< Annex.gitRepo
+	next $ return True
diff --git a/Command/VCycle.hs b/Command/VCycle.hs
--- a/Command/VCycle.hs
+++ b/Command/VCycle.hs
@@ -26,7 +26,7 @@
 start = go =<< currentView
   where
 	go Nothing = giveup "Not in a view."
-	go (Just v) = starting "vcycle" (ActionItemOther Nothing) (SeekInput [])$ do
+	go (Just v) = starting "vcycle" (ActionItemOther Nothing) (SeekInput []) $ do
 		let v' = v { viewComponents = vcycle [] (viewComponents v) }
 		if v == v'
 			then do
diff --git a/Command/Vicfg.hs b/Command/Vicfg.hs
--- a/Command/Vicfg.hs
+++ b/Command/Vicfg.hs
@@ -1,6 +1,6 @@
 {- git-annex command
  -
- - Copyright 2012-2017 Joey Hess <id@joeyh.name>
+ - Copyright 2012-2022 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -75,6 +75,7 @@
 	, cfgScheduleMap :: M.Map UUID [ScheduledActivity]
 	, cfgGlobalConfigs :: M.Map ConfigKey ConfigValue
 	, cfgNumCopies :: Maybe NumCopies
+	, cfgMinCopies :: Maybe MinCopies
 	}
 
 getCfg :: Annex Cfg
@@ -87,6 +88,7 @@
 	<*> scheduleMap
 	<*> loadGlobalConfig
 	<*> getGlobalNumCopies
+	<*> getGlobalMinCopies
 
 setCfg :: Cfg -> Cfg -> Annex ()
 setCfg curcfg newcfg = do
@@ -99,6 +101,7 @@
 	mapM_ (uncurry scheduleSet) $ M.toList $ cfgScheduleMap diff
 	mapM_ (uncurry setGlobalConfig) $ M.toList $ cfgGlobalConfigs diff
 	maybe noop setGlobalNumCopies $ cfgNumCopies diff
+	maybe noop setGlobalMinCopies $ cfgMinCopies diff
 
 {- Default config has all the keys from the input config, but with their
  - default values. -}
@@ -112,6 +115,7 @@
 	, cfgScheduleMap = mapdef $ cfgScheduleMap curcfg
 	, cfgGlobalConfigs = mapdef $ cfgGlobalConfigs curcfg
 	, cfgNumCopies = Nothing
+	, cfgMinCopies = Nothing
 	}
   where
 	mapdef :: forall k v. Default v => M.Map k v -> M.Map k v
@@ -127,6 +131,7 @@
 	, cfgScheduleMap = diff cfgScheduleMap
 	, cfgGlobalConfigs = diff cfgGlobalConfigs
 	, cfgNumCopies = cfgNumCopies newcfg
+	, cfgMinCopies = cfgMinCopies newcfg
 	}
   where
 	diff f = M.differenceWith (\x y -> if x == y then Nothing else Just x)
@@ -236,6 +241,7 @@
 	numcopies =
 		[ com "Numcopies configuration"
 		, line' "numcopies" (show . fromNumCopies <$> cfgNumCopies cfg)
+		, line' "mincopies" (show . fromMinCopies <$> cfgMinCopies cfg)
 		]
 	
 settings :: Ord v => Cfg -> UUIDDescMap -> (Cfg -> M.Map UUID v) -> [String] -> ((v, UUID) -> [String]) -> (UUID -> [String]) -> [String]
@@ -316,6 +322,9 @@
 		| setting == "numcopies" = case readish val of
 			Nothing -> Left "parse error (expected an integer)"
 			Just n -> Right $ cfg { cfgNumCopies = Just (configuredNumCopies n) }
+		| setting == "mincopies" = case readish val of
+			Nothing -> Left "parse error (expected an integer)"
+			Just n -> Right $ cfg { cfgMinCopies = Just (configuredMinCopies n) }
 		| otherwise = badval "setting" setting
 	  where
 		u = toUUID f
diff --git a/Git.hs b/Git.hs
--- a/Git.hs
+++ b/Git.hs
@@ -156,7 +156,7 @@
 #if mingw32_HOST_OS
 	isexecutable f = doesFileExist f
 #else
-	isexecutable f = isExecutable . fileMode <$> getFileStatus f
+	isexecutable f = isExecutable . fileMode <$> getSymbolicLinkStatus f
 #endif
 
 {- Makes the path to a local Repo be relative to the cwd. -}
diff --git a/Git/Config.hs b/Git/Config.hs
--- a/Git/Config.hs
+++ b/Git/Config.hs
@@ -57,12 +57,20 @@
 read' :: Repo -> IO Repo
 read' repo = go repo
   where
-	go Repo { location = Local { gitdir = d } } = git_config d
-	go Repo { location = LocalUnknown d } = git_config d
+	go Repo { location = Local { gitdir = d } } = git_config True d
+	go Repo { location = LocalUnknown d } = git_config False d
 	go _ = assertLocal repo $ error "internal"
-	git_config d = withCreateProcess p (git_config' p)
+	git_config isgitdir d = withCreateProcess p (git_config' p)
 	  where
-		params = ["config", "--null", "--list"]
+		params = 
+			-- Passing --git-dir changes git's behavior
+			-- when run in a repository belonging to another
+			-- user. When a gitdir is known, pass that in order
+			-- to get the local git config.
+			(if isgitdir && gitDirSpecifiedExplicitly repo
+				then ["--git-dir=."]
+				else [])
+			++ ["config", "--null", "--list"]
 		p = (proc "git" params)
 			{ cwd = Just (fromRawFilePath d)
 			, env = gitEnv repo
@@ -282,16 +290,20 @@
  - repo.
  -}
 checkRepoConfigInaccessible :: Repo -> IO Bool
-checkRepoConfigInaccessible r = do
-	-- Cannot use gitCommandLine here because specifying --git-dir
-	-- will bypass the git security check.
-	let p = (proc "git" ["config", "--local", "--list"])
-		{ cwd = Just (fromRawFilePath (repoPath r))
-		, env = gitEnv r
-		}
-	(out, ok) <- processTranscript' p Nothing
-	if not ok
-		then do
-			debug (DebugSource "Git.Config") ("config output: " ++ out)
-			return True
-		else return False
+checkRepoConfigInaccessible r
+	-- When --git-dir or GIT_DIR is used to specify the git
+	-- directory, git does not check for CVE-2022-24765.
+	| gitDirSpecifiedExplicitly r = return False
+	| otherwise = do
+		-- Cannot use gitCommandLine here because specifying --git-dir
+		-- will bypass the git security check.
+		let p = (proc "git" ["config", "--local", "--list"])
+			{ cwd = Just (fromRawFilePath (repoPath r))
+			, env = gitEnv r
+			}
+		(out, ok) <- processTranscript' p Nothing
+		if not ok
+			then do
+				debug (DebugSource "Git.Config") ("config output: " ++ out)
+				return True
+			else return False
diff --git a/Git/Construct.hs b/Git/Construct.hs
--- a/Git/Construct.hs
+++ b/Git/Construct.hs
@@ -277,5 +277,6 @@
 	, gitEnv = Nothing
 	, gitEnvOverridesGitDir = False
 	, gitGlobalOpts = []
+	, gitDirSpecifiedExplicitly = False
 	}
 
diff --git a/Git/Credential.hs b/Git/Credential.hs
--- a/Git/Credential.hs
+++ b/Git/Credential.hs
@@ -1,18 +1,24 @@
 {- git credential interface
  -
- - Copyright 2019-2020 Joey Hess <id@joeyh.name>
+ - Copyright 2019-2022 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
 
+{-# LANGUAGE OverloadedStrings #-}
+
 module Git.Credential where
 
 import Common
 import Git
+import Git.Types
 import Git.Command
+import qualified Git.Config as Config
 import Utility.Url
 
 import qualified Data.Map as M
+import Network.URI
+import Control.Concurrent.STM
 
 data Credential = Credential { fromCredential :: M.Map String String }
 
@@ -27,20 +33,33 @@
 	<$> credentialUsername cred
 	<*> credentialPassword cred
 
-getBasicAuthFromCredential :: Repo -> GetBasicAuth
-getBasicAuthFromCredential r u = do
-	c <- getUrlCredential u r
-	case credentialBasicAuth c of
-		Just ba -> return $ Just (ba, signalsuccess c)
-		Nothing -> do
-			signalsuccess c False
-			return Nothing
+getBasicAuthFromCredential :: Repo -> TMVar CredentialCache -> GetBasicAuth
+getBasicAuthFromCredential r ccv u = do
+	(CredentialCache cc) <- atomically $ readTMVar ccv
+	case mkCredentialBaseURL r u of
+		Just bu -> case M.lookup bu cc of
+			Just c -> go (const noop) c
+			Nothing -> do
+				let storeincache = \c -> atomically $ do
+					(CredentialCache cc') <- takeTMVar ccv
+					putTMVar ccv (CredentialCache (M.insert bu c cc'))
+				go storeincache =<< getUrlCredential u r
+		Nothing -> go (const noop) =<< getUrlCredential u r
   where
-	signalsuccess c True = approveUrlCredential c r
-	signalsuccess c False = rejectUrlCredential c r
+	go storeincache c =
+		case credentialBasicAuth c of
+			Just ba -> return $ Just (ba, signalsuccess)
+			Nothing -> do
+				signalsuccess False
+				return Nothing
+	  where
+		signalsuccess True = do
+			() <- storeincache c
+			approveUrlCredential c r
+		signalsuccess False = rejectUrlCredential c r
 
--- | This may prompt the user for login information, or get cached login
--- information.
+-- | This may prompt the user for the credential, or get a cached
+-- credential from git.
 getUrlCredential :: URLString -> Repo -> IO Credential
 getUrlCredential = runCredential "fill" . urlCredential
 
@@ -79,3 +98,28 @@
 	go l = case break (== '=') l of
 		(k, _:v) -> (k, v)
 		(k, []) -> (k, "")
+
+-- This is not the cache used by git, but is an in-process cache, 
+-- allowing a process to avoid prompting repeatedly when accessing related
+-- urls even when git is not configured to cache credentials.
+data CredentialCache = CredentialCache (M.Map CredentialBaseURL Credential)
+
+-- An url with the uriPath empty when credential.useHttpPath is false.
+--
+-- When credential.useHttpPath is true, no caching is done, since each 
+-- distinct url would need a different credential to be cached, which
+-- could cause the CredentialCache to use a lot of memory. Presumably,
+-- when credential.useHttpPath is false, one Credential is cached
+-- for each git repo accessed, and there are a reasonably small number of
+-- those, so the cache will not grow too large.
+data CredentialBaseURL = CredentialBaseURL URI
+	deriving (Show, Eq, Ord)
+
+mkCredentialBaseURL :: Repo -> URLString -> Maybe CredentialBaseURL
+mkCredentialBaseURL r s = do
+	u <- parseURI s
+	let usehttppath = fromMaybe False $ Config.isTrueFalse' $
+		Config.get (ConfigKey "credential.useHttpPath") (ConfigValue "") r
+	if usehttppath
+		then Nothing
+		else Just $ CredentialBaseURL $ u { uriPath = "" }
diff --git a/Git/CurrentRepo.hs b/Git/CurrentRepo.hs
--- a/Git/CurrentRepo.hs
+++ b/Git/CurrentRepo.hs
@@ -1,6 +1,6 @@
 {- The current git repository.
  -
- - Copyright 2012-2020 Joey Hess <id@joeyh.name>
+ - Copyright 2012-2022 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -79,7 +79,8 @@
 			{ gitdir = absd
 			, worktree = Just curr
 			}
-		r <- Git.Config.read $ newFrom loc
+		r <- Git.Config.read $ (newFrom loc)
+			{ gitDirSpecifiedExplicitly = True }
 		return $ if Git.Config.isBare r
 			then r { location = (location r) { worktree = Nothing } }
 			else r
diff --git a/Git/Queue.hs b/Git/Queue.hs
--- a/Git/Queue.hs
+++ b/Git/Queue.hs
@@ -53,11 +53,11 @@
 	 - those will be run before the FlushAction is. -}
 	| FlushAction
 		{ getFlushActionRunner :: FlushActionRunner m
-		, getFlushActionFiles :: [(RawFilePath, IO Bool, FileSize)]
+		, getFlushActionFiles :: [RawFilePath]
 		}
 
 {- The String must be unique for each flush action. -}
-data FlushActionRunner m = FlushActionRunner String (Repo -> [(RawFilePath, IO Bool, FileSize)] -> m ())
+data FlushActionRunner m = FlushActionRunner String (Repo -> [RawFilePath] -> m ())
 
 instance Eq (FlushActionRunner m) where
 	FlushActionRunner s1 _ == FlushActionRunner s2 _ = s1 == s2
@@ -140,7 +140,7 @@
 {- Adds an flush action to the queue. This can co-exist with anything else
  - that gets added to the queue, and when the queue is eventually flushed,
  - it will be run after the other things in the queue. -}
-addFlushAction :: MonadIO m => FlushActionRunner m -> [(RawFilePath, IO Bool, FileSize)] -> Queue m -> Repo -> m (Queue m)
+addFlushAction :: MonadIO m => FlushActionRunner m -> [RawFilePath] -> Queue m -> Repo -> m (Queue m)
 addFlushAction runner files q repo =
 	updateQueue action (const False) (length files) q repo
   where
diff --git a/Git/Types.hs b/Git/Types.hs
--- a/Git/Types.hs
+++ b/Git/Types.hs
@@ -51,6 +51,8 @@
 	, gitEnvOverridesGitDir :: Bool
 	-- global options to pass to git when running git commands
 	, gitGlobalOpts :: [CommandParam]
+	-- True only when --git-dir or GIT_DIR was used
+	, gitDirSpecifiedExplicitly :: Bool
 	} deriving (Show, Eq, Ord)
 
 newtype ConfigKey = ConfigKey S.ByteString
diff --git a/Git/UpdateIndex.hs b/Git/UpdateIndex.hs
--- a/Git/UpdateIndex.hs
+++ b/Git/UpdateIndex.hs
@@ -1,6 +1,6 @@
 {- git-update-index library
  -
- - Copyright 2011-2020 Joey Hess <id@joeyh.name>
+ - Copyright 2011-2022 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -135,9 +135,17 @@
 indexPath :: TopFilePath -> InternalGitPath
 indexPath = toInternalGitPath . getTopFilePath
 
-{- Refreshes the index, by checking file stat information.  -}
-refreshIndex :: Repo -> ((RawFilePath -> IO ()) -> IO ()) -> IO Bool
-refreshIndex repo feeder = withCreateProcess p go
+{- Refreshes the index, by checking file stat information.
+ -
+ - The action is passed a callback that it can use to send filenames to
+ - update-index. Sending Nothing will wait for update-index to finish
+ - updating the index.
+ -}
+refreshIndex :: (MonadIO m, MonadMask m) => Repo -> ((Maybe RawFilePath -> IO ()) -> m ()) -> m ()
+refreshIndex repo feeder = bracket
+	(liftIO $ createProcess p)
+	(liftIO . cleanupProcess)
+	go
   where
 	params = 
 		[ Param "update-index"
@@ -150,10 +158,13 @@
 	p = (gitCreateProcess params repo)
 		{ std_in = CreatePipe }
 
-	go (Just h) _ _ pid = do
-		feeder $ \f ->
-			S.hPut h (S.snoc f 0)
-		hFlush h
-		hClose h
-		checkSuccessProcess pid
-	go _ _ _ _ = error "internal"
+	go (Just h, _, _, pid) = do
+		let closer = do
+			hFlush h
+			hClose h
+			forceSuccessProcess p pid
+		feeder $ \case
+			Just f -> S.hPut h (S.snoc f 0)
+			Nothing -> closer
+		liftIO $ closer
+	go _ = error "internal"
diff --git a/Limit.hs b/Limit.hs
--- a/Limit.hs
+++ b/Limit.hs
@@ -572,7 +572,7 @@
   where
 	check now k = inAnnexCheck k $ \f ->
 		liftIO $ catchDefaultIO False $ do
-			s <- R.getFileStatus f
+			s <- R.getSymbolicLinkStatus f
 			let accessed = realToFrac (accessTime s)
 			let delta = now - accessed
 			return $ delta <= secs
diff --git a/Logs/File.hs b/Logs/File.hs
--- a/Logs/File.hs
+++ b/Logs/File.hs
@@ -14,6 +14,7 @@
 	modifyLogFile,
 	streamLogFile,
 	checkLogFile,
+	calcLogFile,
 ) where
 
 import Annex.Common
@@ -88,8 +89,8 @@
 -- action is concurrently modifying the file. It does not lock the file,
 -- for speed, but instead relies on the fact that a log file usually
 -- ends in a newline.
-checkLogFile :: FilePath -> RawFilePath -> (L.ByteString -> Bool) -> Annex Bool
-checkLogFile f lck matchf = withExclusiveLock lck $ bracket setup cleanup go
+checkLogFile :: FilePath -> (L.ByteString -> Bool) -> Annex Bool
+checkLogFile f matchf = bracket setup cleanup go
   where
 	setup = liftIO $ tryWhenExists $ openFile f ReadMode
 	cleanup Nothing = noop
@@ -99,6 +100,25 @@
 		!r <- liftIO (any matchf . fullLines <$> L.hGetContents h)
 		return r
 
+-- | Folds a function over lines of a log file to calculate a value.
+--
+-- This can safely be used while appendLogFile or any atomic
+-- action is concurrently modifying the file. It does not lock the file,
+-- for speed, but instead relies on the fact that a log file usually
+-- ends in a newline.
+calcLogFile :: FilePath -> t -> (L.ByteString -> t -> t) -> Annex t
+calcLogFile f start update = bracket setup cleanup go
+  where
+	setup = liftIO $ tryWhenExists $ openFile f ReadMode
+	cleanup Nothing = noop
+	cleanup (Just h) = liftIO $ hClose h
+	go Nothing = return start
+	go (Just h) = go' start =<< liftIO (fullLines <$> L.hGetContents h)
+	go' v [] = return v
+	go' v (l:ls) = do
+		let !v' = update l v
+		go' v' ls
+
 -- | Gets only the lines that end in a newline. If the last part of a file
 -- does not, it's assumed to be a new line being logged that is incomplete,
 -- and is omitted.
@@ -113,25 +133,30 @@
 			let (l, b') = L.splitAt n b
 			in go (l:c) (L.drop 1 b')
 
--- | Streams lines from a log file, and then empties the file at the end.
+-- | Streams lines from a log file, passing each line to the processor,
+-- and then empties the file at the end.
 --
--- If the action is interrupted or throws an exception, the log file is
+-- If the processor is interrupted or throws an exception, the log file is
 -- left unchanged.
 --
--- Does nothing if the log file does not exist.
+-- There is also a finalizer, that is run once all lines have been
+-- streamed. It is run even if the log file does not exist. If the
+-- finalizer throws an exception, the log file is left unchanged.
 -- 
 -- Locking is used to prevent writes to to the log file while this
 -- is running.
-streamLogFile :: FilePath -> RawFilePath -> (String -> Annex ()) -> Annex ()
-streamLogFile f lck a = withExclusiveLock lck $ bracketOnError setup cleanup go
+streamLogFile :: FilePath -> RawFilePath -> Annex () -> (String -> Annex ()) -> Annex ()
+streamLogFile f lck finalizer processor = 
+	withExclusiveLock lck $ bracketOnError setup cleanup go
   where
 	setup = liftIO $ tryWhenExists $ openFile f ReadMode 
 	cleanup Nothing = noop
 	cleanup (Just h) = liftIO $ hClose h
-	go Nothing = noop
+	go Nothing = finalizer
 	go (Just h) = do
-		mapM_ a =<< liftIO (lines <$> hGetContents h)
+		mapM_ processor =<< liftIO (lines <$> hGetContents h)
 		liftIO $ hClose h
+		finalizer
 		liftIO $ writeFile f ""
 		setAnnexFilePerm (toRawFilePath f)
 
diff --git a/Logs/Restage.hs b/Logs/Restage.hs
new file mode 100644
--- /dev/null
+++ b/Logs/Restage.hs
@@ -0,0 +1,63 @@
+{- git-annex restage log file
+ -
+ - Copyright 2022 Joey Hess <id@joeyh.name>
+ -
+ - Licensed under the GNU AGPL version 3 or higher.
+ -}
+
+{-# LANGUAGE OverloadedStrings #-}
+
+module Logs.Restage where
+
+import Annex.Common
+import Git.FilePath
+import Logs.File
+import Utility.InodeCache
+
+import qualified Data.ByteString as S
+import qualified Data.ByteString.Lazy as L
+
+-- | Log a file whose pointer needs to be restaged in git.
+-- The content of the file may not be a pointer, if it is populated with
+-- the annex content. The InodeCache is used to verify that the file
+-- still contains the content, and it's still safe to restage its pointer.
+writeRestageLog :: TopFilePath -> InodeCache -> Annex ()
+writeRestageLog f ic = do
+	logf <- fromRepo gitAnnexRestageLog
+	lckf <- fromRepo gitAnnexRestageLock
+	appendLogFile logf lckf $ L.fromStrict $ formatRestageLog f ic
+
+-- | Streams the content of the restage log, and then empties the log at
+-- the end.
+--
+-- If the processor or finalizer is interrupted or throws an exception,
+-- the log file is left unchanged.
+--
+-- Locking is used to prevent new items being added to the log while this
+-- is running.
+streamRestageLog :: Annex () -> (TopFilePath -> InodeCache -> Annex ()) -> Annex ()
+streamRestageLog finalizer processor = do
+	logf <- fromRepo gitAnnexRestageLog
+	lckf <- fromRepo gitAnnexRestageLock
+	streamLogFile (fromRawFilePath logf) lckf finalizer $ \l -> 
+		case parseRestageLog l of
+			Just (f, ic) -> processor f ic
+			Nothing -> noop
+
+calcRestageLog :: t -> ((TopFilePath, InodeCache) -> t -> t) -> Annex t
+calcRestageLog start update = do
+	logf <- fromRepo gitAnnexRestageLog
+	calcLogFile (fromRawFilePath logf) start $ \l v -> 
+		case parseRestageLog (decodeBL l) of
+			Just pl -> update pl v
+			Nothing -> v
+
+formatRestageLog :: TopFilePath -> InodeCache -> S.ByteString
+formatRestageLog f ic = encodeBS (showInodeCache ic) <> ":" <> getTopFilePath f
+
+parseRestageLog :: String -> Maybe (TopFilePath, InodeCache)
+parseRestageLog l = 
+	let (ics, f) = separate (== ':') l
+	in do
+		ic <- readInodeCache ics
+		return (asTopFilePath (toRawFilePath f), ic)
diff --git a/Logs/Smudge.hs b/Logs/Smudge.hs
--- a/Logs/Smudge.hs
+++ b/Logs/Smudge.hs
@@ -34,7 +34,7 @@
 streamSmudged a = do
 	logf <- fromRepo gitAnnexSmudgeLog
 	lckf <- fromRepo gitAnnexSmudgeLock
-	streamLogFile (fromRawFilePath logf) lckf $ \l -> 
+	streamLogFile (fromRawFilePath logf) lckf noop $ \l -> 
 		case parse l of
 			Nothing -> noop
 			Just (k, f) -> a k f
diff --git a/Remote/Adb.hs b/Remote/Adb.hs
--- a/Remote/Adb.hs
+++ b/Remote/Adb.hs
@@ -360,8 +360,8 @@
 -- connection is resonably fast, it's probably as good as
 -- git's handling of similar situations with files being modified while
 -- it's updating the working tree for a merge.
-retrieveExportWithContentIdentifierM :: AndroidSerial -> AndroidPath -> ExportLocation -> ContentIdentifier -> FilePath -> Either Key (Annex Key) -> MeterUpdate -> Annex (Key, Verification)
-retrieveExportWithContentIdentifierM serial adir loc cid dest gk _p = do
+retrieveExportWithContentIdentifierM :: AndroidSerial -> AndroidPath -> ExportLocation -> [ContentIdentifier] -> FilePath -> Either Key (Annex Key) -> MeterUpdate -> Annex (Key, Verification)
+retrieveExportWithContentIdentifierM serial adir loc cids dest gk _p = do
 	case gk of
 		Right mkkey -> do
 			go
@@ -374,9 +374,10 @@
   where
 	go = do
 		retrieve' serial src dest
-		currcid <- getExportContentIdentifier serial adir loc
-		when (currcid /= Right (Just cid)) $
-			giveup "the file on the android device has changed"
+		getExportContentIdentifier serial adir loc >>= \case
+			Right (Just currcid)
+				| any (currcid ==) cids -> return ()
+			_ -> giveup "the file on the android device has changed"
 	src = androidExportLocation adir loc
 
 storeExportWithContentIdentifierM :: AndroidSerial -> AndroidPath -> FilePath -> Key -> ExportLocation -> [ContentIdentifier] -> MeterUpdate -> Annex ContentIdentifier
diff --git a/Remote/Borg.hs b/Remote/Borg.hs
--- a/Remote/Borg.hs
+++ b/Remote/Borg.hs
@@ -371,7 +371,7 @@
 			, giveup $ "Unable to access borg repository " ++ locBorgRepo borgrepo
 			)
 
-retrieveExportWithContentIdentifierM :: BorgRepo -> ImportLocation -> ContentIdentifier -> FilePath -> Either Key (Annex Key) -> MeterUpdate -> Annex (Key, Verification)
+retrieveExportWithContentIdentifierM :: BorgRepo -> ImportLocation -> [ContentIdentifier] -> FilePath -> Either Key (Annex Key) -> MeterUpdate -> Annex (Key, Verification)
 retrieveExportWithContentIdentifierM borgrepo loc _ dest gk _ = do
 	showOutput
 	case gk of
diff --git a/Remote/Ddar.hs b/Remote/Ddar.hs
--- a/Remote/Ddar.hs
+++ b/Remote/Ddar.hs
@@ -184,7 +184,8 @@
 ddarDirectoryExists :: DdarRepo -> Annex (Either String Bool)
 ddarDirectoryExists ddarrepo
 	| ddarLocal ddarrepo = do
-		maybeStatus <- liftIO $ tryJust (guard . isDoesNotExistError) $ getFileStatus $ ddarRepoLocation ddarrepo
+		maybeStatus <- liftIO $ tryJust (guard . isDoesNotExistError) $
+			getSymbolicLinkStatus $ ddarRepoLocation ddarrepo
 		return $ case maybeStatus of
 			Left _ -> Right False
 			Right status -> Right $ isDirectory status
diff --git a/Remote/Directory.hs b/Remote/Directory.hs
--- a/Remote/Directory.hs
+++ b/Remote/Directory.hs
@@ -218,8 +218,8 @@
 	annexdir <- fromRepo gitAnnexObjectDir
 	samefilesystem <- liftIO $ catchDefaultIO False $ 
 		(\a b -> deviceID a == deviceID b)
-			<$> R.getFileStatus d
-			<*> R.getFileStatus annexdir
+			<$> R.getSymbolicLinkStatus d
+			<*> R.getSymbolicLinkStatus annexdir
 	checkDiskSpace (Just d) k 0 samefilesystem
 
 {- Passed a temp directory that contains the files that should be placed
@@ -369,7 +369,7 @@
 		ImportableContents (catMaybes l') []
   where
 	go f = do
-		st <- R.getFileStatus f
+		st <- R.getSymbolicLinkStatus f
 		mkContentIdentifier ii f st >>= \case
 			Nothing -> return Nothing
 			Just cid -> do
@@ -390,10 +390,24 @@
 			then toInodeCache' noTSDelta f st 0
 			else toInodeCache noTSDelta f st
 
-guardSameContentIdentifiers :: a -> ContentIdentifier -> Maybe ContentIdentifier -> a
-guardSameContentIdentifiers cont old new
-	| new == Just old = cont
+-- Since ignoreinodes can be changed by enableremote, and since previous
+-- versions of git-annex ignored inodes by default, treat two content
+-- idenfiers as the same if they differ only by one having the inode
+-- ignored.
+guardSameContentIdentifiers :: a -> [ContentIdentifier] -> Maybe ContentIdentifier -> a
+guardSameContentIdentifiers _ _ Nothing = giveup "file not found"
+guardSameContentIdentifiers cont olds (Just new)
+	| any (new ==) olds = cont
+	| any (ignoreinode new ==) olds = cont
+	| any (\old -> new == ignoreinode old) olds = cont
 	| otherwise = giveup "file content has changed"
+  where
+	ignoreinode cid@(ContentIdentifier b) = 
+		case readInodeCache (decodeBS b) of
+			Nothing -> cid
+			Just ic -> 
+				let ic' = replaceInode 0 ic
+				in ContentIdentifier (encodeBS (showInodeCache ic'))
 
 importKeyM :: IgnoreInodes -> RawFilePath -> ExportLocation -> ContentIdentifier -> ByteSize -> MeterUpdate -> Annex (Maybe Key)
 importKeyM ii dir loc cid sz p = do
@@ -402,8 +416,8 @@
 	let k = alterKey unsizedk $ \kd -> kd
 		{ keySize = keySize kd <|> Just sz }
 	currcid <- liftIO $ mkContentIdentifier ii absf
-		=<< R.getFileStatus absf
-	guardSameContentIdentifiers (return (Just k)) cid currcid
+		=<< R.getSymbolicLinkStatus absf
+	guardSameContentIdentifiers (return (Just k)) [cid] currcid
   where
 	f = fromExportLocation loc
 	absf = dir P.</> f
@@ -413,8 +427,8 @@
 		, inodeCache = Nothing
 		}
 
-retrieveExportWithContentIdentifierM :: IgnoreInodes -> RawFilePath -> CopyCoWTried -> ExportLocation -> ContentIdentifier -> FilePath -> Either Key (Annex Key) -> MeterUpdate -> Annex (Key, Verification)
-retrieveExportWithContentIdentifierM ii dir cow loc cid dest gk p =
+retrieveExportWithContentIdentifierM :: IgnoreInodes -> RawFilePath -> CopyCoWTried -> ExportLocation -> [ContentIdentifier] -> FilePath -> Either Key (Annex Key) -> MeterUpdate -> Annex (Key, Verification)
+retrieveExportWithContentIdentifierM ii dir cow loc cids dest gk p =
 	case gk of
 		Right mkkey -> do
 			go Nothing
@@ -460,9 +474,9 @@
 	
 	-- Check before copy, to avoid expensive copy of wrong file
 	-- content.
-	precheck cont = guardSameContentIdentifiers cont cid
+	precheck cont = guardSameContentIdentifiers cont cids
 		=<< liftIO . mkContentIdentifier ii f
-		=<< liftIO (R.getFileStatus f)
+		=<< liftIO (R.getSymbolicLinkStatus f)
 
 	-- Check after copy, in case the file was changed while it was
 	-- being copied.
@@ -486,9 +500,9 @@
 #ifndef mingw32_HOST_OS
 			=<< getFdStatus fd
 #else
-			=<< R.getFileStatus f
+			=<< R.getSymbolicLinkStatus f
 #endif
-		guardSameContentIdentifiers cont cid currcid
+		guardSameContentIdentifiers cont cids currcid
 
 	-- When copy-on-write was done, cannot check the handle that was
 	-- copied from, but such a copy should run very fast, so
@@ -497,8 +511,8 @@
 	-- restored to the original content before this check.
 	postcheckcow cont = do
 		currcid <- liftIO $ mkContentIdentifier ii f
-			=<< R.getFileStatus f
-		guardSameContentIdentifiers cont cid currcid
+			=<< R.getSymbolicLinkStatus f
+		guardSameContentIdentifiers cont cids currcid
 
 storeExportWithContentIdentifierM :: IgnoreInodes -> RawFilePath -> CopyCoWTried -> FilePath -> Key -> ExportLocation -> [ContentIdentifier] -> MeterUpdate -> Annex ContentIdentifier
 storeExportWithContentIdentifierM ii dir cow src _k loc overwritablecids p = do
@@ -508,7 +522,7 @@
 		void $ liftIO $ fileCopier cow src tmpf p Nothing
 		let tmpf' = toRawFilePath tmpf
 		resetAnnexFilePerm tmpf'
-		liftIO (getFileStatus tmpf) >>= liftIO . mkContentIdentifier ii tmpf' >>= \case
+		liftIO (getSymbolicLinkStatus tmpf) >>= liftIO . mkContentIdentifier ii tmpf' >>= \case
 			Nothing -> giveup "unable to generate content identifier"
 			Just newcid -> do
 				checkExportContent ii dir loc
@@ -553,7 +567,7 @@
 -- content is known, and immediately run the callback.
 checkExportContent :: IgnoreInodes -> RawFilePath -> ExportLocation -> [ContentIdentifier] -> Annex a -> (CheckResult -> Annex a) -> Annex a
 checkExportContent ii dir loc knowncids unsafe callback = 
-	tryWhenExists (liftIO $ R.getFileStatus dest) >>= \case
+	tryWhenExists (liftIO $ R.getSymbolicLinkStatus dest) >>= \case
 		Just destst
 			| not (isRegularFile destst) -> unsafe
 			| otherwise -> catchDefaultIO Nothing (liftIO $ mkContentIdentifier ii dest destst) >>= \case
diff --git a/Remote/Git.hs b/Remote/Git.hs
--- a/Remote/Git.hs
+++ b/Remote/Git.hs
@@ -350,7 +350,7 @@
 	readlocalannexconfig = do
 		let check = do
 			Annex.BranchState.disableUpdate
-			catchNonAsync autoInitialize $ \e ->
+			catchNonAsync (autoInitialize (pure [])) $ \e ->
 				warning $ "remote " ++ Git.repoDescribe r ++
 					":"  ++ show e
 			Annex.getState Annex.repo
@@ -605,7 +605,7 @@
 	s <- Annex.new r
 	Annex.eval s $ do
 		Annex.BranchState.disableUpdate
-		ensureInitialized
+		ensureInitialized (pure [])
 		a `finally` stopCoProcesses
 
 data LocalRemoteAnnex = LocalRemoteAnnex Git.Repo (MVar [(Annex.AnnexState, Annex.AnnexRead)])
@@ -647,7 +647,7 @@
 	[] -> do
 		liftIO $ putMVar mv []
 		v <- newLocal repo
-		go (v, ensureInitialized >> a)
+		go (v, ensureInitialized (pure []) >> a)
 	(v:rest) -> do
 		liftIO $ putMVar mv rest
 		go (v, a)
diff --git a/Remote/Helper/ExportImport.hs b/Remote/Helper/ExportImport.hs
--- a/Remote/Helper/ExportImport.hs
+++ b/Remote/Helper/ExportImport.hs
@@ -327,11 +327,6 @@
 		db <- getexportdb dbv
 		liftIO $ Export.getExportTree db k
 	
-	getfirstexportloc dbv k = do
-		getexportlocs dbv k >>= \case
-			[] -> giveup "unknown export location"
-			(l:_) -> return l
-	
 	getexportlocs dbv k = do
 		db <- getexportdb dbv
 		liftIO $ Export.getExportTree db k >>= \case
@@ -340,6 +335,15 @@
 				, return []
 				)
 			ls -> return ls
+	
+	tryexportlocs dbv k a = 
+		go Nothing =<< getexportlocs dbv k
+	  where
+		go Nothing [] = giveup "unknown export location"
+		go (Just ex) [] = throwM ex
+		go _ (l:ls) = tryNonAsync (a l) >>= \case
+			Right v -> return v
+			Left e -> go (Just e) ls
 		
 	getkeycids ciddbv k = do
 		db <- getciddb ciddbv
@@ -350,23 +354,22 @@
 	-- have replaced with content not of the requested key, the content
 	-- has to be strongly verified.
 	retrieveKeyFileFromExport dbv k _af dest p = ifM (isVerifiable k)
-		( do
-			l <- getfirstexportloc dbv k
-			retrieveExport (exportActions r) k l dest p >>= return . \case
+		( tryexportlocs dbv k $ \loc -> 
+			retrieveExport (exportActions r) k loc dest p >>= return . \case
 				UnVerified -> MustVerify
 				IncompleteVerify iv -> MustFinishIncompleteVerify iv
 				v -> v
 		, giveup $ "exported content cannot be verified due to using the " ++ decodeBS (formatKeyVariety (fromKey keyVariety k)) ++ " backend"
 		)
 	
-	retrieveKeyFileFromImport dbv ciddbv k af dest p =
-		getkeycids ciddbv k >>= \case
-			(cid:_) -> do
-				l <- getfirstexportloc dbv k
-				snd <$> retrieveExportWithContentIdentifier (importActions r) l cid dest (Left k) p
+	retrieveKeyFileFromImport dbv ciddbv k af dest p = do
+		cids <- getkeycids ciddbv k
+		if not (null cids)
+			then tryexportlocs dbv k $ \loc ->
+				snd <$> retrieveExportWithContentIdentifier (importActions r) loc cids dest (Left k) p
 			-- In case a content identifier is somehow missing,
 			-- try this instead.
-			[] -> if isexport
+			else if isexport
 				then retrieveKeyFileFromExport dbv k af dest p
 				else giveup "no content identifier is recorded, unable to retrieve"
 	
diff --git a/Remote/S3.hs b/Remote/S3.hs
--- a/Remote/S3.hs
+++ b/Remote/S3.hs
@@ -649,8 +649,8 @@
 		| otherwise =
 			i : removemostrecent mtime rest
 
-retrieveExportWithContentIdentifierS3 :: S3HandleVar -> Remote -> RemoteStateHandle -> S3Info -> ExportLocation -> ContentIdentifier -> FilePath -> Either Key (Annex Key) -> MeterUpdate -> Annex (Key, Verification)
-retrieveExportWithContentIdentifierS3 hv r rs info loc cid dest gk p =
+retrieveExportWithContentIdentifierS3 :: S3HandleVar -> Remote -> RemoteStateHandle -> S3Info -> ExportLocation -> [ContentIdentifier] -> FilePath -> Either Key (Annex Key) -> MeterUpdate -> Annex (Key, Verification)
+retrieveExportWithContentIdentifierS3 hv r rs info loc (cid:_) dest gk p =
 	case gk of
 		Right _mkkey -> do
 			k <- go Nothing
@@ -675,6 +675,7 @@
 			return k
 		Nothing -> giveup $ needS3Creds (uuid r)
 	o = T.pack $ bucketExportLocation info loc
+retrieveExportWithContentIdentifierS3 _ _ _ _ _ [] _ _ _ = giveup "missing content identifier"
 
 {- Catch exception getObject returns when a precondition is not met,
  - and replace with a more understandable message for the user. -}
diff --git a/Test.hs b/Test.hs
--- a/Test.hs
+++ b/Test.hs
@@ -1,6 +1,6 @@
 {- git-annex test suite
  -
- - Copyright 2010-2021 Joey Hess <id@joeyh.name>
+ - Copyright 2010-2022 oey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -13,12 +13,12 @@
 import Types.RepoVersion
 import Types.Concurrency
 import Test.Framework
-import Options.Applicative.Types
 
 import Test.Tasty
 import Test.Tasty.HUnit
 import Test.Tasty.QuickCheck
-import Options.Applicative (switch, long, short, help, internal, maybeReader, option)
+import Options.Applicative.Types
+import Options.Applicative (switch, long, short, help, internal, maybeReader, option, metavar)
 
 import qualified Data.Map as M
 import qualified Data.ByteString.Lazy.UTF8 as BU8
@@ -92,7 +92,7 @@
 
 optParser :: Parser TestOptions
 optParser = TestOptions
-	<$> snd (tastyParser (tests 1 False True (TestOptions mempty False False Nothing mempty)))
+	<$> snd (tastyParser (tests 1 False True (TestOptions mempty False False Nothing mempty mempty)))
 	<*> switch
 		( long "keep-failures"
 		<> help "preserve repositories on test failure"
@@ -106,7 +106,19 @@
 		<> short 'J'
 		<> help "number of concurrent jobs"
 		))
+	<*> many (option (maybeReader parseconfigvalue)
+		( long "test-git-config"
+		<> help "run tests with a git config set"
+		<> metavar "NAME=VALUE"
+		))
 	<*> cmdParams "non-options are for internal use only"
+  where
+	parseconfigvalue s = case break (== '=') s of
+		(_, []) -> Nothing
+		(k, v) -> Just 
+			( Git.Types.ConfigKey (encodeBS k)
+			, Git.Types.ConfigValue (encodeBS (drop 1 v))
+			)
 
 runner :: TestOptions -> IO ()
 runner opts = parallelTestRunner opts tests
diff --git a/Test/Framework.hs b/Test/Framework.hs
--- a/Test/Framework.hs
+++ b/Test/Framework.hs
@@ -169,7 +169,7 @@
 	case r of
 		Right () -> return ()
 		Left e -> do
-			whenM (keepFailures <$> getTestMode) $
+			whenM (keepFailuresOption . testOptions <$> getTestMode) $
 				putStrLn $ "** Preserving repo for failure analysis in " ++ clone
 			throwM e
 
@@ -255,6 +255,13 @@
 	-- tell git-annex to not annex the ingitfile
 	git "config" ["annex.largefiles", "exclude=" ++ ingitfile]
 		"git config annex.largefiles"
+	-- set any additional git configs the user wants to test with
+	gc <- testGitConfig . testOptions <$> getTestMode
+	forM_ gc $ \case
+		(Git.Types.ConfigKey k, Git.Types.ConfigValue v) -> 
+			git "config" [decodeBS k, decodeBS v]
+				"git config from test options"
+		(Git.Types.ConfigKey _, Git.Types.NoConfigValue) -> noop
 
 ensuredir :: FilePath -> IO ()
 ensuredir d = do
@@ -483,15 +490,15 @@
 	{ unlockedFiles :: Bool
 	, adjustedUnlockedBranch :: Bool
 	, annexVersion :: Types.RepoVersion.RepoVersion
-	, keepFailures :: Bool
-	} deriving (Show)
+	, testOptions :: TestOptions
+	}
 
 testMode :: TestOptions -> Types.RepoVersion.RepoVersion -> TestMode
 testMode opts v = TestMode
 	{ unlockedFiles = False
 	, adjustedUnlockedBranch = False
 	, annexVersion = v
-	, keepFailures = keepFailuresOption opts
+	, testOptions = opts
 	}
 
 hasUnlockedFiles :: TestMode -> Bool
diff --git a/Types/GitConfig.hs b/Types/GitConfig.hs
--- a/Types/GitConfig.hs
+++ b/Types/GitConfig.hs
@@ -153,7 +153,7 @@
 	, annexUUID = hereuuid
 	, annexNumCopies = configuredNumCopies
 		<$> getmayberead (annexConfig "numcopies")
-	, annexDiskReserve = fromMaybe onemegabyte $
+	, annexDiskReserve = fromMaybe (onemegabyte * 100) $
 		readSize dataUnits =<< getmaybe (annexConfig "diskreserve")
 	, annexDirect = getbool (annexConfig "direct") False
 	, annexBackend = maybe
diff --git a/Types/Remote.hs b/Types/Remote.hs
--- a/Types/Remote.hs
+++ b/Types/Remote.hs
@@ -334,7 +334,7 @@
 	-- Throws exception on failure to access the remote.
 	, importKey :: Maybe (ImportLocation -> ContentIdentifier -> ByteSize -> MeterUpdate -> a (Maybe Key))
 	-- Retrieves a file from the remote. Ensures that the file
-	-- it retrieves has the requested ContentIdentifier.
+	-- it retrieves has one of the requested ContentIdentifiers.
 	--
 	-- This has to be used rather than retrieveExport
 	-- when a special remote supports imports, since files on such a
@@ -343,7 +343,7 @@
 	-- Throws exception on failure.
 	, retrieveExportWithContentIdentifier 
 		:: ExportLocation
-		-> ContentIdentifier
+		-> [ContentIdentifier]
 		-- file to write content to
 		-> FilePath
 		-- Either the key, or when it's not yet known, a callback
diff --git a/Types/Test.hs b/Types/Test.hs
--- a/Types/Test.hs
+++ b/Types/Test.hs
@@ -11,12 +11,14 @@
 
 import Types.Concurrency
 import Types.Command
+import Git.Types
 
 data TestOptions = TestOptions
 	{ tastyOptionSet :: OptionSet
 	, keepFailuresOption :: Bool
 	, fakeSsh :: Bool
 	, concurrentJobs :: Maybe Concurrency
+	, testGitConfig :: [(ConfigKey, ConfigValue)]
 	, internalData :: CmdParams
 	}
 
diff --git a/Upgrade.hs b/Upgrade.hs
--- a/Upgrade.hs
+++ b/Upgrade.hs
@@ -74,31 +74,24 @@
 		Left ex -> err $ "Automatic upgrade exception! " ++ show ex
 
 upgrade :: Bool -> RepoVersion -> Annex Bool
-upgrade automatic destversion = do
-	startversion <- getVersion
-	(ok, newversion) <- go startversion
-	when (ok && newversion /= startversion) $
-		postupgrade newversion
-	return ok
+upgrade automatic destversion = go =<< getVersion
   where
 	go (Just v)
-		| v >= destversion = return (True, Just v)
+		| v >= destversion = return True
 		| otherwise = ifM upgradingRemote
 			( upgraderemote
 			, up v >>= \case
-				UpgradeSuccess -> go (Just (incrversion v) )
-				UpgradeFailed -> return (False, Just v)
-				UpgradeDeferred -> return (True, Just v)
+				UpgradeSuccess -> do
+					let v' = incrversion v
+					upgradedto v'
+					go (Just v')
+				UpgradeFailed -> return False
+				UpgradeDeferred -> return True
 			)
-	go Nothing = return (True, Nothing)
+	go Nothing = return True
 
 	incrversion v = RepoVersion (fromRepoVersion v + 1)
 
-	postupgrade newversion = ifM upgradingRemote
-		( reloadConfig
-		, maybe noop upgradedto newversion
-		)
-
 #ifndef mingw32_HOST_OS
 	up (RepoVersion 0) = Upgrade.V0.upgrade
 	up (RepoVersion 1) = Upgrade.V1.upgrade
@@ -121,15 +114,18 @@
 	-- upgrading a git repo other than the current repo.
 	upgraderemote = do
 		rp <- fromRawFilePath <$> fromRepo Git.repoPath
-		gitAnnexChildProcess "upgrade"
+		ok <- gitAnnexChildProcess "upgrade"
 			[ Param "--quiet"
 			, Param "--autoonly"
 			]
 			(\p -> p { cwd = Just rp })
 			(\_ _ _ pid -> waitForProcess pid >>= return . \case
-				ExitSuccess -> (True, Nothing)
-				_ -> (False, Nothing)
+				ExitSuccess -> True
+				_ -> False
 			)
+		when ok
+			reloadConfig
+		return ok
 
 	upgradedto v = do
 		setVersion v
diff --git a/Upgrade/V9.hs b/Upgrade/V9.hs
--- a/Upgrade/V9.hs
+++ b/Upgrade/V9.hs
@@ -46,7 +46,7 @@
 				then return True
 				else assistantrunning
 		-- Initialized at v9, so no old process danger exists.
-		Nothing -> return False
+		Nothing -> pure False
 
 	{- Skip upgrade when git-annex assistant (or watch) is running,
 	 - because these are long-running daemons that could conceivably
diff --git a/Utility/InodeCache.hs b/Utility/InodeCache.hs
--- a/Utility/InodeCache.hs
+++ b/Utility/InodeCache.hs
@@ -32,6 +32,7 @@
 	inodeCacheToMtime,
 	inodeCacheToEpochTime,
 	inodeCacheEpochTimeRange,
+	replaceInode,
 
 	SentinalFile(..),
 	SentinalStatus(..),
@@ -125,6 +126,10 @@
 	let t = inodeCacheToEpochTime i
 	in (t-1, t+1)
 
+replaceInode :: FileID -> InodeCache -> InodeCache
+replaceInode inode (InodeCache (InodeCachePrim _ sz mtime)) =
+	InodeCache (InodeCachePrim inode sz mtime)
+
 {- For backwards compatability, support low-res mtime with no
  - fractional seconds. -}
 data MTime = MTimeLowRes EpochTime | MTimeHighRes POSIXTime
@@ -187,7 +192,7 @@
 
 genInodeCache :: RawFilePath -> TSDelta -> IO (Maybe InodeCache)
 genInodeCache f delta = catchDefaultIO Nothing $
-	toInodeCache delta f =<< R.getFileStatus f
+	toInodeCache delta f =<< R.getSymbolicLinkStatus f
 
 toInodeCache :: TSDelta -> RawFilePath -> FileStatus -> IO (Maybe InodeCache)
 toInodeCache d f s = toInodeCache' d f s (fileID s)
diff --git a/Utility/MagicWormhole.hs b/Utility/MagicWormhole.hs
--- a/Utility/MagicWormhole.hs
+++ b/Utility/MagicWormhole.hs
@@ -119,17 +119,22 @@
 			(findcode ph herr)
 		return (inout || inerr)
   where
-	p = wormHoleProcess (Param "send" : ps ++ [File f])
-	findcode ph h = findcode' =<< getwords ph h []
+	p = wormHoleProcess (ps ++ [Param "send", File f])
+	findcode ph h = hGetLineUntilExitOrEOF ph h >>= \case
+		Nothing -> return False
+		Just l -> ifM (findcode' (words l))
+			( drain ph h >> return True
+			, findcode ph h
+			)
 	findcode' [] = return False
 	findcode' (w:ws) = case mkCode w of
 		Just code -> do
 			_ <- tryPutMVar observer code
 			return True
 		Nothing -> findcode' ws
-	getwords ph h c = hGetLineUntilExitOrEOF ph h >>= \case
-		Nothing -> return $ concatMap words $ reverse c
-		Just l -> getwords ph h (l:c)
+	drain ph h = hGetLineUntilExitOrEOF ph h >>= \case
+		Just _ -> drain ph h
+		Nothing -> return ()
 
 -- | Receives a file. Once the receive is under way, the Code will be
 -- read from the CodeProducer, and fed to wormhole on stdin.
@@ -140,12 +145,12 @@
 	hFlush hin
 	return True
   where
-	p = wormHoleProcess $
+	p = wormHoleProcess $ ps ++
 		[ Param "receive"
 		, Param "--accept-file"
 		, Param "--output-file"
 		, File f
-		] ++ ps
+		]
 
 wormHoleProcess :: WormHoleParams -> CreateProcess
 wormHoleProcess = proc "wormhole" . toCommand
diff --git a/Utility/MoveFile.hs b/Utility/MoveFile.hs
--- a/Utility/MoveFile.hs
+++ b/Utility/MoveFile.hs
@@ -72,7 +72,7 @@
 
 #ifndef mingw32_HOST_OS	
 	isdir f = do
-		r <- tryIO $ R.getFileStatus f
+		r <- tryIO $ R.getSymbolicLinkStatus f
 		case r of
 			(Left _) -> return False
 			(Right s) -> return $ isDirectory s
diff --git a/Utility/Url.hs b/Utility/Url.hs
--- a/Utility/Url.hs
+++ b/Utility/Url.hs
@@ -313,7 +313,7 @@
 
 	existsfile u = do
 		let f = toRawFilePath (unEscapeString (uriPath u))
-		s <- catchMaybeIO $ R.getFileStatus f
+		s <- catchMaybeIO $ R.getSymbolicLinkStatus f
 		case s of
 			Just stat -> do
 				sz <- getFileSize' f stat
diff --git a/doc/git-annex-backends.mdwn b/doc/git-annex-backends.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/git-annex-backends.mdwn
@@ -0,0 +1,24 @@
+# NAME
+
+git-annex-backends - key/value backends for git-annex
+
+# DESCRIPTION
+
+The "backend" in git-annex controls how a key is generated from a file's
+content and/or filesystem metadata. Most backends are different kinds of
+hashes. A single repository can use different backends for different files.
+
+For a list of available backends, see `git-annex version`. For more
+details, see <https://git-annex.branchable.com/backends/>
+
+# SEE ALSO
+
+[[git-annex]](1)
+
+# AUTHOR
+
+Joey Hess <id@joeyh.name>
+
+<http://git-annex.branchable.com/>
+
+Warning: Automatically converted into a man page by mdwn2man. Edit with care.
diff --git a/doc/git-annex-dead.mdwn b/doc/git-annex-dead.mdwn
--- a/doc/git-annex-dead.mdwn
+++ b/doc/git-annex-dead.mdwn
@@ -17,8 +17,8 @@
 description, or their UUID. (To undo, use `git-annex semitrust`.)
 
 When a key is specified, indicates that the content of that key has been
-irretrievably lost. This prevents commands like `git annex fsck --all`
-from complaining about it. 
+irretrievably lost. This makes the key be skipped when operating
+on all keys with eg `--all`.
 (To undo, add the key's content back to the repository, 
 by using eg, `git-annex reinject`.)
 
diff --git a/doc/git-annex-log.mdwn b/doc/git-annex-log.mdwn
--- a/doc/git-annex-log.mdwn
+++ b/doc/git-annex-log.mdwn
@@ -46,7 +46,7 @@
 
 * `--all` `-A`
 
-  Shows location log changes to all files, with the most recent changes first.
+  Shows location log changes to all content, with the most recent changes first.
   In this mode, the names of files are not available and keys are displayed
   instead.
 
diff --git a/doc/git-annex-migrate.mdwn b/doc/git-annex-migrate.mdwn
--- a/doc/git-annex-migrate.mdwn
+++ b/doc/git-annex-migrate.mdwn
@@ -57,6 +57,8 @@
 
 [[git-annex-upgrade]](1)
 
+[[git-annex-backend]](1)
+
 # AUTHOR
 
 Joey Hess <id@joeyh.name>
diff --git a/doc/git-annex-preferred-content.mdwn b/doc/git-annex-preferred-content.mdwn
--- a/doc/git-annex-preferred-content.mdwn
+++ b/doc/git-annex-preferred-content.mdwn
@@ -99,10 +99,12 @@
   Like lackingcopies, but does not look at .gitattributes annex.numcopies
   settings. This makes it significantly faster.
 
-* `inbackend=name`
+* `inbackend=backendname`
 
   Matches only files whose content is stored using the specified key-value
   backend.
+
+  See [[git-annex-backends]](1) for information about available backends.
 
 * `securehash`
 
diff --git a/doc/git-annex-restage.mdwn b/doc/git-annex-restage.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/git-annex-restage.mdwn
@@ -0,0 +1,34 @@
+# NAME
+
+git-annex restage - restages unlocked files in the git index
+
+# SYNOPSIS
+
+git annex restage
+
+# DESCRIPTION
+
+Since getting or dropping an unlocked file modifies the file in the work
+tree, git needs to be told that the modification does not change the
+content that it has recorded (the annex pointer). Restaging the file
+accomplishes that.
+
+You do not normally need to run this command, because usually git-annex
+is able to restage unlocked files itself. There are some situations
+where git-annex needs to restage a file, but the git index is locked,
+and so it cannot. It will then display a warning suggesting you run this
+command.
+
+It's safe to run this command even after you have made a modification to an
+unlocked file.
+
+# SEE ALSO
+
+[[git-annex]](1)
+[[git-annex-smudge]](1)
+
+# AUTHOR
+
+Joey Hess <id@joeyh.name>
+
+Warning: Automatically converted into a man page by mdwn2man. Edit with care.
diff --git a/doc/git-annex-test.mdwn b/doc/git-annex-test.mdwn
--- a/doc/git-annex-test.mdwn
+++ b/doc/git-annex-test.mdwn
@@ -30,6 +30,20 @@
   When there are test failures, leave the `.t` directory populated with
   repositories that demonstate the failures, for later analysis.
 
+* `--test-git-config name=value`
+
+  The test suite prevents git from reading any git configuration files.
+  Usually it is a good idea to run the test suite with a standard 
+  git configuration. However, this option can be useful to see what
+  effect a git configuration setting has on the test suite. 
+
+  Some configuration settings will break the test suite, in ways that are
+  due to a bug in git-annex. But it is possible that changing a
+  configuration can find a legitimate bug in git-annex.
+
+  One valid use of this is to change a git configuration to a value that
+  is planned to be the new default in a future version of git.
+
 # SEE ALSO
 
 [[git-annex]](1)
diff --git a/doc/git-annex-whereis.mdwn b/doc/git-annex-whereis.mdwn
--- a/doc/git-annex-whereis.mdwn
+++ b/doc/git-annex-whereis.mdwn
@@ -38,6 +38,9 @@
 * `--all` `-A`
 
   Show whereis information for all known keys.
+  
+  (Except for keys that have been marked as dead,
+  see [[git-annex-dead]](1).)
 
 * `--branch=ref`
 
diff --git a/doc/git-annex.mdwn b/doc/git-annex.mdwn
--- a/doc/git-annex.mdwn
+++ b/doc/git-annex.mdwn
@@ -708,8 +708,8 @@
 * `smudge`
 
   This command lets git-annex be used as a git filter driver, allowing
-  annexed files in the git repository to be unlocked at all times, instead
-  of being symlinks.
+  annexed files in the git repository to be unlocked regular files instead
+  of symlinks.
 
   See [[git-annex-smudge]](1) for details.
 
@@ -720,6 +720,12 @@
 
   See [[git-annex-filter-process]](1) for details.
 
+* `restage`
+  
+  Restages unlocked files in the git index.
+
+  See [[git-annex-restage]](1) for details.
+
 * `findref [ref]`
 
   Lists files in a git ref. (deprecated)
@@ -786,7 +792,8 @@
 * `annex.backend`
 
   Name of the default key-value backend to use when adding new files
-  to the repository.
+  to the repository. See [[git-annex-backends]](1) for information about
+  available backends.
 
   This is overridden by annex annex.backend configuration in the
   .gitattributes files, and by the --backend option.
@@ -822,12 +829,12 @@
 * `annex.diskreserve`
 
   Amount of disk space to reserve. Disk space is checked when transferring
-  content to avoid running out, and additional free space can be reserved
-  via this option, to make space for more important content (such as git
-  commit logs). Can be specified with any commonly used units, for example,
-  "0.5 gb", "500M", or "100 KiloBytes"
+  annexed content to avoid running out, and additional free space can be
+  reserved via this option, to make space for other data (such as git 
+  commit logs). Can be specified with any commonly used units, for 
+  example, "0.5 gb", "500M", or "100 KiloBytes"
 
-  The default reserve is 1 megabyte.
+  The default reserve is 100 megabytes.
 
 * `annex.skipunknown`
 
@@ -1273,6 +1280,9 @@
   In the command line, %path is replaced with the file or directory to
   operate on.
 
+  (When annex.crippledfilesystem is set, git-annex will not try to
+  remove/restore the write bit, but it will still run these hooks.)
+
 * `annex.tune.objecthash1`, `annex.tune.objecthashlower`, `annex.tune.branchhash1`
 
   These can be passed to `git annex init` to tune the repository.
@@ -1698,12 +1708,15 @@
   (rather than the default built-in url downloader).
 
   For example, to force IPv4 only, set it to "-4".
-  Or to make curl use your ~/.netrc file, set it to "--netrc".
 
   Setting this option makes git-annex use curl, but only
   when annex.security.allowed-ip-addresses is configured in a
   specific way. See its documentation.
 
+  Setting this option prevents git-annex from using git-credential
+  for prompting for http passwords. Instead, you can include "--netrc"
+  to make curl use your ~/.netrc file and record the passwords there.
+
 * `annex.youtube-dl-options`
 
   Options to pass to youtube-dl when using it to find the url to download
@@ -1896,7 +1909,9 @@
 The key-value backend used when adding a new file to the annex can be
 configured on a per-file-type basis via `.gitattributes` files. In the file,
 the `annex.backend` attribute can be set to the name of the backend to
-use. For example, this here's how to use the WORM backend by default,
+use. (See [[git-annex-backends]](1) for information about
+available backends.)
+For example, this here's how to use the WORM backend by default,
 but the SHA256E backend for ogg files:
 
 	* annex.backend=WORM
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: 10.20220822
+Version: 10.20220927
 Cabal-Version: 1.12
 License: AGPL-3
 Maintainer: Joey Hess <id@joeyh.name>
@@ -46,6 +46,7 @@
   doc/git-annex-addurl.mdwn
   doc/git-annex-adjust.mdwn
   doc/git-annex-assistant.mdwn
+  doc/git-annex-backends.mdwn
   doc/git-annex-calckey.mdwn
   doc/git-annex-checkpresentkey.mdwn
   doc/git-annex-config.mdwn
@@ -109,6 +110,7 @@
   doc/git-annex-renameremote.mdwn
   doc/git-annex-repair.mdwn
   doc/git-annex-required.mdwn
+  doc/git-annex-restage.mdwn
   doc/git-annex-resolvemerge.mdwn
   doc/git-annex-rmurl.mdwn
   doc/git-annex-schedule.mdwn
@@ -256,10 +258,8 @@
   Utility/libkqueue.h
 
 Flag Assistant
-  Description: Enable git-annex assistant and watch command
-
-Flag Webapp
-  Description: Enable git-annex webapp
+  Description: Enable git-annex assistant, webapp, and watch command
+  Default: True
 
 Flag Pairing
   Description: Enable pairing
@@ -376,7 +376,21 @@
    aws (>= 0.20),
    DAV (>= 1.0),
    network (>= 3.0.0.0),
-   network-bsd
+   network-bsd,
+   mountpoints,
+   yesod (>= 1.4.3), 
+   yesod-static (>= 1.5.1),
+   yesod-form (>= 1.4.8),
+   yesod-core (>= 1.6.0),
+   path-pieces (>= 0.2.1),
+   warp (>= 3.2.8),
+   warp-tls (>= 3.2.2),
+   wai,
+   wai-extra,
+   blaze-builder,
+   clientsession,
+   template-haskell,
+   shakespeare (>= 2.0.11)
   CC-Options: -Wall
   GHC-Options: -Wall -fno-warn-tabs  -Wincomplete-uni-patterns
   Default-Language: Haskell2010
@@ -416,8 +430,7 @@
     Other-Modules: Utility.GitLFS
   
   if flag(Assistant) && ! os(solaris) && ! os(gnu)
-    Build-Depends: mountpoints
-    CPP-Options: -DWITH_ASSISTANT
+    CPP-Options: -DWITH_ASSISTANT -DWITH_WEBAPP
     Other-Modules:
       Assistant
       Assistant.Alert
@@ -436,6 +449,7 @@
       Assistant.Install.AutoStart
       Assistant.Install.Menu
       Assistant.MakeRemote
+      Assistant.MakeRepo
       Assistant.Monad
       Assistant.NamedThread
       Assistant.Pairing
@@ -458,6 +472,7 @@
       Assistant.Threads.Merger
       Assistant.Threads.MountWatcher
       Assistant.Threads.NetWatcher
+      Assistant.Threads.PairListener
       Assistant.Threads.ProblemFixer
       Assistant.Threads.Pusher
       Assistant.Threads.RemoteControl
@@ -469,6 +484,7 @@
       Assistant.Threads.UpgradeWatcher
       Assistant.Threads.Upgrader
       Assistant.Threads.Watcher
+      Assistant.Threads.WebApp
       Assistant.TransferQueue
       Assistant.TransferSlots
       Assistant.Types.Alert
@@ -489,11 +505,44 @@
       Assistant.Types.UrlRenderer
       Assistant.Unused
       Assistant.Upgrade
+      Assistant.WebApp
+      Assistant.WebApp.Common
+      Assistant.WebApp.Configurators
+      Assistant.WebApp.Configurators.AWS
+      Assistant.WebApp.Configurators.Delete
+      Assistant.WebApp.Configurators.Edit
+      Assistant.WebApp.Configurators.Fsck
+      Assistant.WebApp.Configurators.IA
+      Assistant.WebApp.Configurators.Local
+      Assistant.WebApp.Configurators.Pairing
+      Assistant.WebApp.Configurators.Preferences
+      Assistant.WebApp.Configurators.Ssh
+      Assistant.WebApp.Configurators.Unused
+      Assistant.WebApp.Configurators.Upgrade
+      Assistant.WebApp.Configurators.WebDAV
+      Assistant.WebApp.Control
+      Assistant.WebApp.DashBoard
+      Assistant.WebApp.Documentation
+      Assistant.WebApp.Form
+      Assistant.WebApp.Gpg
+      Assistant.WebApp.MakeRemote
+      Assistant.WebApp.Notifications
+      Assistant.WebApp.OtherRepos
+      Assistant.WebApp.Page
+      Assistant.WebApp.Pairing
+      Assistant.WebApp.Repair
+      Assistant.WebApp.RepoId
+      Assistant.WebApp.RepoList
+      Assistant.WebApp.SideBar
+      Assistant.WebApp.Types
       Command.Assistant
       Command.Watch
+      Command.WebApp
       Utility.Mounts
       Utility.OSX
-
+      Utility.Yesod
+      Utility.WebApp
+    
     if os(linux)
       Build-Depends: hinotify (>= 0.3.10)
       CPP-Options: -DWITH_INOTIFY
@@ -516,60 +565,6 @@
             Includes: Utility/libkqueue.h
             Other-Modules: Utility.DirWatcher.Kqueue
   
-    if flag(Webapp)
-      Build-Depends:
-       yesod (>= 1.4.3), 
-       yesod-static (>= 1.5.1),
-       yesod-form (>= 1.4.8),
-       yesod-core (>= 1.6.0),
-       path-pieces (>= 0.2.1),
-       warp (>= 3.2.8),
-       warp-tls (>= 3.2.2),
-       wai,
-       wai-extra,
-       blaze-builder,
-       clientsession,
-       template-haskell,
-       shakespeare (>= 2.0.11)
-      CPP-Options: -DWITH_WEBAPP
-      Other-Modules:
-        Command.WebApp
-        Assistant.Threads.WebApp
-        Assistant.Threads.PairListener
-        Assistant.WebApp
-        Assistant.WebApp.Common
-        Assistant.WebApp.Configurators
-        Assistant.WebApp.Configurators.AWS
-        Assistant.WebApp.Configurators.Delete
-        Assistant.WebApp.Configurators.Edit
-        Assistant.WebApp.Configurators.Fsck
-        Assistant.WebApp.Configurators.IA
-        Assistant.WebApp.Configurators.Local
-        Assistant.WebApp.Configurators.Pairing
-        Assistant.WebApp.Configurators.Preferences
-        Assistant.WebApp.Configurators.Ssh
-        Assistant.WebApp.Configurators.Unused
-        Assistant.WebApp.Configurators.Upgrade
-        Assistant.WebApp.Configurators.WebDAV
-        Assistant.WebApp.Control
-        Assistant.WebApp.DashBoard
-        Assistant.WebApp.Documentation
-        Assistant.WebApp.Form
-        Assistant.WebApp.Gpg
-        Assistant.WebApp.MakeRemote
-        Assistant.WebApp.Notifications
-        Assistant.WebApp.OtherRepos
-        Assistant.WebApp.Page
-        Assistant.WebApp.Pairing
-        Assistant.WebApp.Repair
-        Assistant.WebApp.RepoId
-        Assistant.WebApp.RepoList
-        Assistant.WebApp.SideBar
-        Assistant.WebApp.Types
-        Assistant.MakeRepo
-        Utility.Yesod
-        Utility.WebApp
-
   if flag(Dbus)
     if (os(linux))
       Build-Depends: dbus (>= 0.10.7), fdo-notify (>= 0.3)
@@ -782,6 +777,7 @@
     Command.Repair
     Command.Required
     Command.ResolveMerge
+    Command.Restage
     Command.RmUrl
     Command.Schedule
     Command.Semitrust
@@ -916,6 +912,7 @@
     Logs.Remote
     Logs.Remote.Pure
     Logs.RemoteState
+    Logs.Restage
     Logs.Schedule
     Logs.SingleValue
     Logs.SingleValue.Pure
diff --git a/stack.yaml b/stack.yaml
--- a/stack.yaml
+++ b/stack.yaml
@@ -4,7 +4,6 @@
     assistant: true
     pairing: true
     torrentparser: true
-    webapp: true
     magicmime: false
     dbus: false
     debuglocks: false
