diff --git a/Annex/Content.hs b/Annex/Content.hs
--- a/Annex/Content.hs
+++ b/Annex/Content.hs
@@ -43,6 +43,7 @@
 	moveBad,
 	KeyLocation(..),
 	listKeys,
+	listKeys',
 	saveState,
 	downloadUrl,
 	preseedTmp,
@@ -653,22 +654,26 @@
  - .git/annex/objects, whether or not the content is present.
  -}
 listKeys :: KeyLocation -> Annex [Key]
-listKeys keyloc = do
+listKeys keyloc = listKeys' keyloc (const (pure True))
+
+{- Due to use of unsafeInterleaveIO, the passed filter action
+ - will be run in a copy of the Annex state, so any changes it
+ - makes to the state will not be preserved. -}
+listKeys' :: KeyLocation -> (Key -> Annex Bool) -> Annex [Key]
+listKeys' keyloc want = do
 	dir <- fromRepo gitAnnexObjectDir
-	{- In order to run Annex monad actions within unsafeInterleaveIO,
-	 - the current state is taken and reused. No changes made to this
-	 - state will be preserved. 
-	 -}
 	s <- Annex.getState id
+	r <- Annex.getRead id
 	depth <- gitAnnexLocationDepth <$> Annex.getGitConfig
-	liftIO $ walk s depth (fromRawFilePath dir)
+	liftIO $ walk (s, r) depth (fromRawFilePath dir)
   where
 	walk s depth dir = do
 		contents <- catchDefaultIO [] (dirContents dir)
 		if depth < 2
 			then do
-				contents' <- filterM (present s) contents
-				let keys = mapMaybe (fileKey . P.takeFileName . toRawFilePath) contents'
+				contents' <- filterM present contents
+				keys <- filterM (Annex.eval s . want) $
+					mapMaybe (fileKey . P.takeFileName . toRawFilePath) contents'
 				continue keys []
 			else do
 				let deeper = walk s (depth - 1)
@@ -683,8 +688,8 @@
 		InAnywhere -> True
 		_ -> False
 
-	present _ _ | inanywhere = pure True
-	present _ d = presentInAnnex d
+	present _ | inanywhere = pure True
+	present d = presentInAnnex d
 
 	presentInAnnex = doesFileExist . contentfile
 	contentfile d = d </> takeFileName d
diff --git a/Annex/Link.hs b/Annex/Link.hs
--- a/Annex/Link.hs
+++ b/Annex/Link.hs
@@ -190,7 +190,7 @@
 		-- 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.addInternalAction runner [(absf, isunmodified tsd, inodeCacheFileSize orig)]
+		Annex.Queue.addFlushAction runner [(absf, isunmodified tsd, inodeCacheFileSize orig)]
   where
 	isunmodified tsd = genInodeCache f tsd >>= return . \case
 		Nothing -> False
@@ -202,8 +202,8 @@
 	-- 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.InternalActionRunner Annex
-	runner = Git.Queue.InternalActionRunner "restagePointerFile" $ \r l -> do
+	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
diff --git a/Annex/Queue.hs b/Annex/Queue.hs
--- a/Annex/Queue.hs
+++ b/Annex/Queue.hs
@@ -9,7 +9,7 @@
 
 module Annex.Queue (
 	addCommand,
-	addInternalAction,
+	addFlushAction,
 	addUpdateIndex,
 	flush,
 	flushWhenFull,
@@ -31,11 +31,11 @@
 	store =<< flushWhenFull =<<
 		(Git.Queue.addCommand commonparams command params files q =<< gitRepo)
 
-addInternalAction :: Git.Queue.InternalActionRunner Annex -> [(RawFilePath, IO Bool, FileSize)] -> Annex ()
-addInternalAction runner files = do
+addFlushAction :: Git.Queue.FlushActionRunner Annex -> [(RawFilePath, IO Bool, FileSize)] -> Annex ()
+addFlushAction runner files = do
 	q <- get
 	store =<< flushWhenFull =<<
-		(Git.Queue.addInternalAction runner files q =<< gitRepo)
+		(Git.Queue.addFlushAction runner files q =<< gitRepo)
 
 {- Adds an update-index stream to the queue. -}
 addUpdateIndex :: Git.UpdateIndex.Streamer -> Annex ()
diff --git a/CHANGELOG b/CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,22 @@
+git-annex (10.20220222) upstream; urgency=medium
+
+  * annex.skipunknown now defaults to false, so commands like
+    `git annex get foo*` will not silently skip over files/dirs that are
+    not checked into git.
+  * info: Allow using matching options in more situations. File matching
+    options like --include will be rejected in situations where there is
+    no filename to match against.
+  * adb: Avoid find failing with "Argument list too long"
+  * Fix git-annex forget propagation between repositories.
+    (reversion introduced in version 7.20190122)
+  * registerurl, unregisterurl: Improved output when reading from stdin
+    to be more like other batch commands.
+  * registerurl, unregisterurl: Added --json and --json-error-messages options.
+  * Avoid git status taking a long time after git-annex unlock of many files.
+  * Pass --no-textconv when running git diff internally.
+
+ -- Joey Hess <id@joeyh.name>  Tue, 22 Feb 2022 13:01:20 -0400
+
 git-annex (10.20220127) upstream; urgency=medium
 
   * New v10 repository version (with v9 as a stepping-stone to it).
diff --git a/Command/FromKey.hs b/Command/FromKey.hs
--- a/Command/FromKey.hs
+++ b/Command/FromKey.hs
@@ -86,12 +86,15 @@
 -- the uri scheme, to see if it looks like the prefix of a key. This relies
 -- on key backend names never containing a ':'.
 keyOpt :: String -> Key
-keyOpt s = case parseURI s of
+keyOpt = either giveup id . keyOpt'
+
+keyOpt' :: String -> Either String Key
+keyOpt' s = case parseURI s of
 	Just u | not (isKeyPrefix (uriScheme u)) ->
-		Backend.URL.fromUrl s Nothing
+		Right $ Backend.URL.fromUrl s Nothing
 	_ -> case deserializeKey s of
-		Just k -> k
-		Nothing -> giveup $ "bad key/url " ++ s
+		Just k -> Right k
+		Nothing -> Left $ "bad key/url " ++ s
 
 perform :: AddUnlockedMatcher -> Key -> RawFilePath -> CommandPerform
 perform matcher key file = lookupKeyNotHidden file >>= \case
diff --git a/Command/Info.hs b/Command/Info.hs
--- a/Command/Info.hs
+++ b/Command/Info.hs
@@ -1,6 +1,6 @@
 {- git-annex command
  -
- - Copyright 2011-2021 Joey Hess <id@joeyh.name>
+ - Copyright 2011-2022 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -132,7 +132,6 @@
 
 globalInfo :: InfoOptions -> Annex ()
 globalInfo o = do
-	disallowMatchingOptions
 	u <- getUUID
 	whenM ((==) DeadTrusted <$> lookupTrust u) $
 		earlyWarning "Warning: This repository is currently marked as dead."
@@ -145,7 +144,6 @@
 itemInfo o (si, p) = ifM (isdir p)
 	( dirInfo o p si
 	, do
-		disallowMatchingOptions
 		v <- Remote.byName' p
 		case v of
 			Right r -> remoteInfo o r si
@@ -168,10 +166,6 @@
 	showNote $ "not a directory or an annexed file or a treeish or a remote or a uuid"
 	showEndFail
 
-disallowMatchingOptions :: Annex ()
-disallowMatchingOptions = whenM Limit.limited $
-	giveup "File matching options can only be used when getting info on a directory."
-
 dirInfo :: InfoOptions -> FilePath -> SeekInput -> Annex ()
 dirInfo o dir si = showCustom (unwords ["info", dir]) si $ do
 	stats <- selStats
@@ -197,9 +191,13 @@
 	tostats = map (\s -> s t)
 
 fileInfo :: InfoOptions -> FilePath -> SeekInput -> Key -> Annex ()
-fileInfo o file si k = showCustom (unwords ["info", file]) si $ do
-	evalStateT (mapM_ showStat (file_stats file k)) (emptyStatInfo o)
-	return True
+fileInfo o file si k = do
+	matcher <- Limit.getMatcher
+	let file' = toRawFilePath file
+	whenM (matcher $ MatchingFile $ FileInfo file' file' (Just k)) $
+		showCustom (unwords ["info", file]) si $ do
+			evalStateT (mapM_ showStat (file_stats file k)) (emptyStatInfo o)
+			return True
 
 remoteInfo :: InfoOptions -> Remote -> SeekInput -> Annex ()
 remoteInfo o r si = showCustom (unwords ["info", Remote.name r]) si $ do
@@ -404,7 +402,7 @@
 bad_data_size = staleSize "bad keys size" gitAnnexBadDir
 
 key_size :: Key -> Stat
-key_size k = simpleStat "size" $ showSizeKeys $ foldKeys [k]
+key_size k = simpleStat "size" $ showSizeKeys $ addKey k emptyKeyInfo
 
 key_name :: Key -> Stat
 key_name k = simpleStat "key" $ pure $ serializeKey k
@@ -525,7 +523,9 @@
 	case presentData s of
 		Just v -> return v
 		Nothing -> do
-			v <- foldKeys <$> lift (listKeys InAnnex)
+			matcher <- lift getKeyOnlyMatcher
+			v <- foldl' (flip addKey) emptyKeyInfo
+				<$> lift (listKeys' InAnnex (matchOnKey matcher))
 			put s { presentData = Just v }
 			return v
 
@@ -535,9 +535,13 @@
 	case M.lookup u (repoData s) of
 		Just v -> return (Right v)
 		Nothing -> do
+			matcher <- lift getKeyOnlyMatcher
 			let combinedata d uk = finishCheck uk >>= \case
 				Nothing -> return d
-				Just k -> return $ addKey k d
+				Just k -> ifM (matchOnKey matcher k)
+					( return (addKey k d)
+					, return d
+					)
 			lift (loggedKeysFor' u) >>= \case
 				Just (ks, cleanup) -> do
 					v <- lift $ foldM combinedata emptyKeyInfo ks
@@ -552,8 +556,13 @@
 	case referencedData s of
 		Just v -> return v
 		Nothing -> do
+			matcher <- lift getKeyOnlyMatcher
+			let combinedata k _f d = ifM (matchOnKey matcher k)
+				( return (addKey k d)
+				, return d
+				)
 			!v <- lift $ Command.Unused.withKeysReferenced
-				emptyKeyInfo addKey
+				emptyKeyInfo combinedata
 			put s { referencedData = Just v }
 			return v
 
@@ -596,11 +605,16 @@
 getTreeStatInfo :: InfoOptions -> Git.Ref -> Annex (Maybe StatInfo)
 getTreeStatInfo o r = do
 	fast <- Annex.getState Annex.fast
+	-- git lstree filenames start with a leading "./" that prevents
+	-- matching, and also things like --include are supposed to
+	-- match relative to the current directory, which does not make
+	-- sense when matching against files in some arbitrary tree.
+	matcher <- getKeyOnlyMatcher
 	(ls, cleanup) <- inRepo $ LsTree.lsTree
 		LsTree.LsTreeRecursive
 		(LsTree.LsTreeLong False)
 		r
-	(presentdata, referenceddata, repodata) <- go fast ls initial
+	(presentdata, referenceddata, repodata) <- go fast matcher ls initial
 	ifM (liftIO cleanup)
 		( return $ Just $
 			StatInfo (Just presentdata) (Just referenceddata) repodata Nothing o
@@ -608,23 +622,25 @@
 		)
   where
 	initial = (emptyKeyInfo, emptyKeyInfo, M.empty)
-	go _ [] vs = return vs
-	go fast (l:ls) vs@(presentdata, referenceddata, repodata) = do
-		mk <- catKey (LsTree.sha l)
-		case mk of
-			Nothing -> go fast ls vs
-			Just key -> do
-				!presentdata' <- ifM (inAnnex key)
-					( return $ addKey key presentdata
-					, return presentdata
-					)
-				let !referenceddata' = addKey key referenceddata
-				!repodata' <- if fast
-					then return repodata
-					else do
-						locs <- Remote.keyLocations key
-						return (updateRepoData key locs repodata)
-				go fast ls $! (presentdata', referenceddata', repodata')
+	go _ _ [] vs = return vs
+	go fast matcher (l:ls) vs@(presentdata, referenceddata, repodata) =
+		catKey (LsTree.sha l) >>= \case
+			Nothing -> go fast matcher ls vs
+			Just key -> ifM (matchOnKey matcher key)
+				( do
+					!presentdata' <- ifM (inAnnex key)
+						( return $ addKey key presentdata
+						, return presentdata
+						)
+					let !referenceddata' = addKey key referenceddata
+					!repodata' <- if fast
+						then return repodata
+						else do
+							locs <- Remote.keyLocations key
+							return (updateRepoData key locs repodata)
+					go fast matcher ls $! (presentdata', referenceddata', repodata')
+				, go fast matcher ls vs
+				)
 
 emptyKeyInfo :: KeyInfo
 emptyKeyInfo = KeyInfo 0 0 0 M.empty
@@ -632,9 +648,6 @@
 emptyNumCopiesStats :: NumCopiesStats
 emptyNumCopiesStats = NumCopiesStats M.empty
 
-foldKeys :: [Key] -> KeyInfo
-foldKeys = foldl' (flip addKey) emptyKeyInfo
-
 addKey :: Key -> KeyInfo -> KeyInfo
 addKey key (KeyInfo count size unknownsize backends) =
 	KeyInfo count' size' unknownsize' backends'
@@ -700,3 +713,20 @@
 	( return (const $ const show)
 	, return roughSize
 	)
+			
+getKeyOnlyMatcher :: Annex (MatchInfo -> Annex Bool)
+getKeyOnlyMatcher = do
+	whenM (Limit.introspect matchNeedsFileName) $ do
+		warning "File matching options cannot be applied when getting this info."
+		giveup "Unable to continue."
+	Limit.getMatcher
+
+matchOnKey :: (MatchInfo -> Annex Bool) -> Key -> Annex Bool
+matchOnKey matcher k = matcher $ MatchingInfo $ ProvidedInfo
+	{ providedFilePath = Nothing
+	, providedKey = Just k
+	, providedFileSize = Nothing
+	, providedMimeType = Nothing
+	, providedMimeEncoding = Nothing
+	, providedLinkType = Nothing
+	}
diff --git a/Command/RegisterUrl.hs b/Command/RegisterUrl.hs
--- a/Command/RegisterUrl.hs
+++ b/Command/RegisterUrl.hs
@@ -1,21 +1,19 @@
 {- git-annex command
  -
- - Copyright 2015-2018 Joey Hess <id@joeyh.name>
+ - Copyright 2015-2022 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
 
-{-# LANGUAGE BangPatterns #-}
-
 module Command.RegisterUrl where
 
 import Command
 import Logs.Web
-import Command.FromKey (keyOpt)
+import Command.FromKey (keyOpt, keyOpt')
 import qualified Remote
 
 cmd :: Command
-cmd = command "registerurl"
+cmd = withGlobalOptions [jsonOptions] $ command "registerurl"
 	SectionPlumbing "registers an url for a key"
 	(paramPair paramKey paramUrl)
 	(seek <$$> optParser)
@@ -32,45 +30,39 @@
 
 seek :: RegisterUrlOptions -> CommandSeek
 seek o = case (batchOption o, keyUrlPairs o) of
-	(Batch (BatchFormat sep _), _) -> batchOnly Nothing (keyUrlPairs o) $
-		commandAction $ startMass setUrlPresent sep
+	(Batch fmt, _) -> seekBatch setUrlPresent o fmt
 	-- older way of enabling batch input, does not support BatchNull
-	(NoBatch, []) -> commandAction $ startMass setUrlPresent BatchLine
-	(NoBatch, ps) -> withWords (commandAction . start setUrlPresent) ps
+	(NoBatch, []) -> seekBatch setUrlPresent o (BatchFormat BatchLine (BatchKeys False))
+	(NoBatch, ps) -> commandAction (start setUrlPresent ps)
 
+seekBatch :: (Key -> URLString -> Annex ()) -> RegisterUrlOptions -> BatchFormat -> CommandSeek
+seekBatch a o fmt = batchOnly Nothing (keyUrlPairs o) $
+	batchInput fmt (pure . parsebatch) $
+		batchCommandAction . start' a
+  where
+	parsebatch l = 
+		let (keyname, u) = separate (== ' ') l
+		in if null u
+			then Left "no url provided"
+			else case keyOpt' keyname of
+				Left e -> Left e
+				Right k -> Right (k, u)
+
 start :: (Key -> URLString -> Annex ()) -> [String] -> CommandStart
-start a (keyname:url:[]) = 
-	starting "registerurl" ai si $
-		perform a (keyOpt keyname) url
+start a (keyname:url:[]) = start' a (si, (keyOpt keyname, url))
   where
-	ai = ActionItemOther (Just url)
 	si = SeekInput [keyname, url]
 start _ _ = giveup "specify a key and an url"
 
-startMass :: (Key -> URLString -> Annex ()) -> BatchSeparator -> CommandStart
-startMass a sep =
-	starting "registerurl" (ActionItemOther (Just "stdin")) (SeekInput []) $
-		performMass a sep
-
-performMass :: (Key -> URLString -> Annex ()) -> BatchSeparator -> CommandPerform
-performMass a sep = go True =<< map (separate (== ' ')) <$> batchLines fmt
+start' :: (Key -> URLString -> Annex ()) -> (SeekInput, (Key, URLString)) -> CommandStart
+start' a (si, (key, url)) =
+	starting "registerurl" ai si $
+		perform a key url
   where
-	fmt = BatchFormat sep (BatchKeys False)
-	go status [] = next $ return status
-	go status ((keyname,u):rest) | not (null keyname) && not (null u) = do
-		let key = keyOpt keyname
-		ok <- perform' a key u
-		let !status' = status && ok
-		go status' rest
-	go _ _ = giveup "Expected pairs of key and url on stdin, but got something else."
+	ai = ActionItemOther (Just url)
 
 perform :: (Key -> URLString -> Annex ()) -> Key -> URLString -> CommandPerform
 perform a key url = do
-	ok <- perform' a key url
-	next $ return ok
-
-perform' :: (Key -> URLString -> Annex ()) -> Key -> URLString -> Annex Bool
-perform' a key url = do
 	r <- Remote.claimingUrl url
 	a key (setDownloader' url r)
-	return True
+	next $ return True
diff --git a/Command/Unlock.hs b/Command/Unlock.hs
--- a/Command/Unlock.hs
+++ b/Command/Unlock.hs
@@ -1,6 +1,6 @@
 {- git-annex command
  -
- - Copyright 2010-2016 Joey Hess <id@joeyh.name>
+ - Copyright 2010-2022 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -12,6 +12,8 @@
 import Annex.Perms
 import Annex.Link
 import Annex.ReplaceFile
+import Annex.InodeSentinal
+import Utility.InodeCache
 import Git.FilePath
 import qualified Database.Keys
 import qualified Utility.RawFilePath as R
@@ -47,7 +49,7 @@
 perform :: RawFilePath -> Key -> CommandPerform
 perform dest key = do
 	destmode <- liftIO $ catchMaybeIO $ fileMode <$> R.getFileStatus dest
-	replaceWorkTreeFile (fromRawFilePath dest) $ \tmp ->
+	destic <- replaceWorkTreeFile (fromRawFilePath dest) $ \tmp -> do
 		ifM (inAnnex key)
 			( do
 				r <- linkFromAnnex' key (toRawFilePath tmp) destmode
@@ -57,10 +59,12 @@
 					LinkAnnexFailed -> error "unlock failed"
 			, liftIO $ writePointerFile (toRawFilePath tmp) key destmode
 			)
-	next $ cleanup dest key destmode
+		withTSDelta (liftIO . genInodeCache (toRawFilePath tmp))
+	next $ cleanup dest destic key destmode
 
-cleanup ::  RawFilePath -> Key -> Maybe FileMode -> CommandCleanup
-cleanup dest key destmode = do
+cleanup :: RawFilePath -> Maybe InodeCache -> Key -> Maybe FileMode -> CommandCleanup
+cleanup dest destic key destmode = do
 	stagePointerFile dest destmode =<< hashPointerFile key
+	maybe noop (restagePointerFile (Restage True) dest) destic
 	Database.Keys.addAssociatedFile key =<< inRepo (toTopFilePath dest)
 	return True
diff --git a/Command/UnregisterUrl.hs b/Command/UnregisterUrl.hs
--- a/Command/UnregisterUrl.hs
+++ b/Command/UnregisterUrl.hs
@@ -1,6 +1,6 @@
 {- git-annex command
  -
- - Copyright 2015-2021 Joey Hess <id@joeyh.name>
+ - Copyright 2015-2022 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -11,18 +11,18 @@
 
 import Command
 import Logs.Web
-import Command.RegisterUrl (start, startMass, optParser, RegisterUrlOptions(..))
+import Command.RegisterUrl (seekBatch, start, optParser, RegisterUrlOptions(..))
 
 cmd :: Command
-cmd = command "unregisterurl"
+cmd = withGlobalOptions [jsonOptions] $ command "unregisterurl"
 	SectionPlumbing "unregisters an url for a key"
 	(paramPair paramKey paramUrl)
 	(seek <$$> optParser)
 
 seek :: RegisterUrlOptions -> CommandSeek
 seek o = case (batchOption o, keyUrlPairs o) of
-	(Batch (BatchFormat sep _), _) -> commandAction $ startMass unregisterUrl sep
-	(NoBatch, ps) -> withWords (commandAction . start unregisterUrl) ps
+	(Batch fmt, _) -> seekBatch unregisterUrl o fmt
+	(NoBatch, ps) -> commandAction (start unregisterUrl ps)
 
 unregisterUrl :: Key -> String -> Annex ()
 unregisterUrl key url = do
diff --git a/Command/Unused.hs b/Command/Unused.hs
--- a/Command/Unused.hs
+++ b/Command/Unused.hs
@@ -183,12 +183,10 @@
 	runfilter a l = a l
 	runbloomfilter a = runfilter $ \l -> bloomFilter l <$> genBloomFilter a
 
-{- Given an initial value, folds it with each key referenced by
- - files in the working tree. -}
-withKeysReferenced :: v -> (Key -> v -> v) -> Annex v
-withKeysReferenced initial a = withKeysReferenced' Nothing initial folda
-  where
-	folda k _ v = return $ a k v
+{- Given an initial value, accumulates the value over each key
+ - referenced by files in the working tree. -}
+withKeysReferenced :: v -> (Key -> RawFilePath -> v -> Annex v) -> Annex v
+withKeysReferenced initial = withKeysReferenced' Nothing initial
 
 {- Runs an action on each referenced key in the working tree. -}
 withKeysReferencedM :: (Key -> Annex ()) -> Annex ()
diff --git a/Database/Keys.hs b/Database/Keys.hs
--- a/Database/Keys.hs
+++ b/Database/Keys.hs
@@ -342,6 +342,9 @@
 		, Param "--no-renames"
 		-- Avoid other complications.
 		, Param "--ignore-submodules=all"
+		-- Avoid using external textconv command, which would be slow
+		-- and possibly wrong.
+		, Param "--no-textconv"
 		, Param "--no-ext-diff"
 		]
 	
diff --git a/Git/Queue.hs b/Git/Queue.hs
--- a/Git/Queue.hs
+++ b/Git/Queue.hs
@@ -1,6 +1,6 @@
 {- git repository command queue
  -
- - 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.
  -}
@@ -13,8 +13,8 @@
 	defaultTimelimit,
 	addCommand,
 	addUpdateIndex,
-	addInternalAction,
-	InternalActionRunner(..),
+	addFlushAction,
+	FlushActionRunner(..),
 	size,
 	full,
 	flush,
@@ -48,30 +48,34 @@
 		-- ^ parameters that come after the git subcommand
 		, getFiles :: [CommandParam]
 		} 
-	{- An internal action to run, on a list of files that can be added
-	 - to as the queue grows. -}
-	| InternalAction
-		{ getRunner :: InternalActionRunner m
-		, getInternalFiles :: [(RawFilePath, IO Bool, FileSize)]
+	{- A FlushAction can be added along with CommandActions or
+	 - UpdateIndexActions, and when the queue later gets flushed,
+	 - those will be run before the FlushAction is. -}
+	| FlushAction
+		{ getFlushActionRunner :: FlushActionRunner m
+		, getFlushActionFiles :: [(RawFilePath, IO Bool, FileSize)]
 		}
 
-{- The String must be unique for each internal action. -}
-data InternalActionRunner m = InternalActionRunner String (Repo -> [(RawFilePath, IO Bool, FileSize)] -> m ())
+{- The String must be unique for each flush action. -}
+data FlushActionRunner m = FlushActionRunner String (Repo -> [(RawFilePath, IO Bool, FileSize)] -> m ())
 
-instance Eq (InternalActionRunner m) where
-	InternalActionRunner s1 _ == InternalActionRunner s2 _ = s1 == s2
+instance Eq (FlushActionRunner m) where
+	FlushActionRunner s1 _ == FlushActionRunner s2 _ = s1 == s2
 
-{- A key that can uniquely represent an action in a Map. -}
+{- A key that can uniquely represent an action in a Map.
+ -
+ - The ordering controls what order the actions are run in when flushing
+ - the queue. -}
 data ActionKey
 	= UpdateIndexActionKey
 	| CommandActionKey [CommandParam] String [CommandParam]
-	| InternalActionKey String
+	| FlushActionKey String
 	deriving (Eq, Ord)
 
 actionKey :: Action m -> ActionKey
 actionKey (UpdateIndexAction _) = UpdateIndexActionKey
 actionKey CommandAction { getCommonParams = c, getSubcommand = s, getParams = p } = CommandActionKey c s p
-actionKey InternalAction { getRunner = InternalActionRunner s _ } = InternalActionKey s
+actionKey FlushAction { getFlushActionRunner = FlushActionRunner s _ } = FlushActionKey s
 
 {- A queue of actions to perform (in any order) on a git repository,
  - with lists of files to perform them on. This allows coalescing 
@@ -120,7 +124,7 @@
  -}
 addCommand :: MonadIO m => [CommandParam] -> String -> [CommandParam] -> [FilePath] -> Queue m -> Repo -> m (Queue m)
 addCommand commonparams subcommand params files q repo =
-	updateQueue action different (length files) q repo
+	updateQueue action conflicting (length files) q repo
   where
 	action = CommandAction
 		{ getCommonParams = commonparams
@@ -129,36 +133,37 @@
 		, getFiles = map File files
 		}
 	
-	different (CommandAction { getSubcommand = s }) = s /= subcommand
-	different _ = True
+	conflicting (CommandAction { getSubcommand = s }) = s /= subcommand
+	conflicting (FlushAction {}) = False
+	conflicting _ = True
 
-{- Adds an internal action to the queue. -}
-addInternalAction :: MonadIO m => InternalActionRunner m -> [(RawFilePath, IO Bool, FileSize)] -> Queue m -> Repo -> m (Queue m)
-addInternalAction runner files q repo =
-	updateQueue action different (length files) q repo
+{- 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 runner files q repo =
+	updateQueue action (const False) (length files) q repo
   where
-	action = InternalAction
-		{ getRunner = runner
-		, getInternalFiles = files
+	action = FlushAction
+		{ getFlushActionRunner = runner
+		, getFlushActionFiles = files
 		}
-	
-	different (InternalAction { getRunner = r }) = r /= runner
-	different _ = True
 
 {- Adds an update-index streamer to the queue. -}
 addUpdateIndex :: MonadIO m => Git.UpdateIndex.Streamer -> Queue m -> Repo -> m (Queue m)
 addUpdateIndex streamer q repo =
-	updateQueue action different 1 q repo
+	updateQueue action conflicting 1 q repo
   where
 	-- the list is built in reverse order
 	action = UpdateIndexAction [streamer]
 
-	different (UpdateIndexAction _) = False
-	different _ = True
+	conflicting (UpdateIndexAction _) = False
+	conflicting (FlushAction {}) = False
+	conflicting _ = True
 
 {- Updates or adds an action in the queue.
  -
- - If the queue already contains a different action, it will be flushed
+ - If the queue already contains a conflicting action, it will be flushed
  - before adding the action; this is to ensure that conflicting actions,
  - like add and rm, are run in the right order.
  -
@@ -166,19 +171,19 @@
  - and the action will be run right away.
  -}
 updateQueue :: MonadIO m => Action m -> (Action m -> Bool) -> Int -> Queue m -> Repo -> m (Queue m)
-updateQueue !action different sizeincrease q repo = do
+updateQueue !action conflicting sizeincrease q repo = do
 	now <- liftIO getPOSIXTime
 	if now - (_lastchanged q) > _timelimit q
-		then if isdifferent
+		then if isconflicting
 			then do
 				q' <- flush q repo
 				flush (mk q') repo
 			else flush (mk q) repo
-		else if isdifferent
+		else if isconflicting
 			then mk <$> flush q repo
 			else return $ mk (q { _lastchanged = now })
   where
-	isdifferent = not (null (filter different (M.elems (items q))))
+	isconflicting = not (null (filter conflicting (M.elems (items q))))
 	mk q' = newq
 	  where		
 		!newq = q'
@@ -196,8 +201,8 @@
 	CommandAction cps2 sc2 ps2 (fs1++fs2)
 combineNewOld (UpdateIndexAction s1) (UpdateIndexAction s2) =
 	UpdateIndexAction (s1++s2)
-combineNewOld (InternalAction _r1 fs1) (InternalAction r2 fs2) =
-	InternalAction r2 (fs1++fs2)
+combineNewOld (FlushAction _r1 fs1) (FlushAction r2 fs2) =
+	FlushAction r2 (fs1++fs2)
 combineNewOld anew _aold = anew
 
 {- Merges the contents of the second queue into the first.
@@ -257,6 +262,6 @@
 		forceSuccessProcess p pid
 	go _ _ _ _ _ = error "internal"
 #endif
-runAction repo action@(InternalAction {}) =
-	let InternalActionRunner _ runner = getRunner action
-	in runner repo (getInternalFiles action)
+runAction repo action@(FlushAction {}) =
+	let FlushActionRunner _ runner = getFlushActionRunner action
+	in runner repo (getFlushActionFiles action)
diff --git a/Logs/Transitions.hs b/Logs/Transitions.hs
--- a/Logs/Transitions.hs
+++ b/Logs/Transitions.hs
@@ -25,6 +25,7 @@
 
 import qualified Data.Set as S
 import Data.Either
+import Data.Char
 import Data.ByteString (ByteString)
 import Data.ByteString.Builder
 import qualified Data.ByteString.Lazy as L
@@ -82,7 +83,7 @@
 
 transitionLineParser :: A.Parser TransitionLine
 transitionLineParser = do
-	t <- (parsetransition <$> A.takeByteString)
+	t <- parsetransition <$> A.takeTill (== sp)
 	_ <- A8.char ' '
 	c <- vectorClockParser
 	return $ TransitionLine c t
@@ -90,6 +91,7 @@
 	parsetransition b = case readish (decodeBS b) of
 		Just t -> Right t
 		Nothing -> Left b
+	sp = fromIntegral (ord ' ')
 
 combineTransitions :: [Transitions] -> Transitions
 combineTransitions = S.unions
diff --git a/NEWS b/NEWS
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,12 @@
+git-annex (10.20220218) upstream; urgency=medium
+
+  Transition notice: Commands like `git-annex get foo*` have changed to error
+  out when some of files are not checked into the repository. To get back the
+  previous behavior of silently skipping unknown files, use git config
+  to set annex.skipunknown to true.
+
+ -- Joey Hess <id@joeyh.name>  Fri, 18 Feb 2022 13:12:21 -0400
+
 git-annex (8.20211028) upstream; urgency=medium
 
   This version of git-annex removes support for communicating with git-annex
diff --git a/Remote/Adb.hs b/Remote/Adb.hs
--- a/Remote/Adb.hs
+++ b/Remote/Adb.hs
@@ -307,11 +307,15 @@
 		-- won't recurse into the directory
 		, File $ fromAndroidPath adir ++ "/"
 		, Param "-type", Param "f"
-		, Param "-exec", Param "stat"
-		, Param "-c", Param statformat
-		, Param "{}", Param "+"
+		, Param "-printf", Param "%p\\0"
 		] 
-		(if ignorefinderror then "|| true" else "")
+		(\s -> concat
+			[ "set -o pipefail; "
+			, s
+			, "| xargs -0 stat -c " ++ shellEscape statformat ++ " --"
+			, if ignorefinderror then " || true" else ""
+			]
+		)
 
 	ignorefinderror = fromMaybe False (getRemoteConfigValue ignorefinderrorField c)
 
@@ -399,11 +403,11 @@
 --
 -- Any stdout from the command is returned, separated into lines.
 adbShell :: AndroidSerial -> [CommandParam] -> Annex (Maybe [String])
-adbShell serial cmd = adbShell' serial cmd ""
+adbShell serial cmd = adbShell' serial cmd id
 
-adbShell' :: AndroidSerial -> [CommandParam] -> String -> Annex (Maybe [String])
-adbShell' serial cmd extra = adbShellRaw serial $
-	(unwords $ map shellEscape (toCommand cmd)) ++ extra
+adbShell' :: AndroidSerial -> [CommandParam] -> (String -> String) -> Annex (Maybe [String])
+adbShell' serial cmd f = adbShellRaw serial $
+	f (unwords $ map shellEscape (toCommand cmd))
 
 adbShellBool :: AndroidSerial -> [CommandParam] -> Annex Bool
 adbShellBool serial cmd =
diff --git a/Test.hs b/Test.hs
--- a/Test.hs
+++ b/Test.hs
@@ -360,6 +360,7 @@
 	, testCase "union merge regression" test_union_merge_regression
 	, testCase "adjusted branch merge regression" test_adjusted_branch_merge_regression
 	, testCase "adjusted branch subtree regression" test_adjusted_branch_subtree_regression
+	, testCase "transition propagation" test_transition_propagation_reversion
 	, testCase "conflict resolution" test_conflict_resolution
 	, testCase "conflict resolution (adjusted branch)" test_conflict_resolution_adjusted_branch
 	, testCase "conflict resolution movein regression" test_conflict_resolution_movein_regression
@@ -2051,3 +2052,38 @@
 		-- Make sure that import did not import the file to the top
 		-- of the repo.
 		checkdoesnotexist annexedfile
+
+test_transition_propagation_reversion :: Assertion
+test_transition_propagation_reversion =
+	withtmpclonerepo $ \r1 ->
+		withtmpclonerepo $ \r2 -> do
+			pair r1 r2
+			indir r1 $ do
+				disconnectOrigin
+				writecontent wormannexedfile $ content wormannexedfile
+				git_annex "add" [wormannexedfile, "--backend=WORM"] "add"
+				git_annex "sync" [] "sync"
+			indir r2 $ do
+				disconnectOrigin
+				git_annex "sync" [] "sync"
+			indir r1 $ do
+				git_annex "sync" [] "sync"
+			indir r2 $ do
+				git_annex "get" [wormannexedfile] "get"
+				git_annex "drop" [wormannexedfile] "drop"
+				git_annex "get" [wormannexedfile] "get"
+				git_annex "drop" [wormannexedfile] "drop"
+			indir r1 $ do
+				git_annex "drop" ["--force", wormannexedfile] "drop"
+				git_annex "sync" [] "sync"
+				git_annex "forget" ["--force"] "forget"
+				git_annex "sync" [] "sync"
+				emptylog
+			indir r2 $ do
+				git_annex "sync" [] "sync"
+				emptylog
+			indir r1 $ do
+				git_annex "sync" [] "sync"
+				emptylog
+  where
+	emptylog = git_annex_expectoutput "log" [wormannexedfile] []
diff --git a/Types/GitConfig.hs b/Types/GitConfig.hs
--- a/Types/GitConfig.hs
+++ b/Types/GitConfig.hs
@@ -233,7 +233,7 @@
 	, annexCommitMode = if getbool (annexConfig "allowsign") False
 		then ManualCommit
 		else AutomaticCommit
-	, annexSkipUnknown = getbool (annexConfig "skipunknown") True
+	, annexSkipUnknown = getbool (annexConfig "skipunknown") False
 	, annexAdjustedBranchRefresh = fromMaybe
 		-- parse as bool if it's not a number
 		(if getbool "adjustedbranchrefresh" False then 1 else 0)
diff --git a/doc/git-annex-drop.mdwn b/doc/git-annex-drop.mdwn
--- a/doc/git-annex-drop.mdwn
+++ b/doc/git-annex-drop.mdwn
@@ -45,7 +45,8 @@
 * `--auto`
 
   Rather than trying to drop all specified files, drop only those that
-  are not preferred content of the repository.
+  are not preferred content of the repository, and avoid trying to drop
+  files when there are not enough other copies for the drop to be possible.
   See [[git-annex-preferred-content]](1)
 
 * `--force`
diff --git a/doc/git-annex-info.mdwn b/doc/git-annex-info.mdwn
--- a/doc/git-annex-info.mdwn
+++ b/doc/git-annex-info.mdwn
@@ -11,9 +11,9 @@
 Displays statistics and other information for the specified item,
 which can be a directory, or a file, or a treeish, or a remote,
 or the description or uuid of a repository.
-  
+
 When no item is specified, displays statistics and information
-for the local repository and all known annexed files.
+for the local repository and all annexed content.
 
 # OPTIONS
 
@@ -45,11 +45,10 @@
   Makes the `--batch` input be delimited by nulls instead of the usual
   newlines.
 
-* file matching options
+* matching options
 
-  When a directory is specified, the [[git-annex-matching-options]](1)
-  can be used to select the files in the directory that are included
-  in the statistics.
+  The [[git-annex-matching-options]](1) can be used to select what
+  to include in the statistics.
 
 * Also the [[git-annex-common-options]](1) can be used.
 
diff --git a/doc/git-annex-registerurl.mdwn b/doc/git-annex-registerurl.mdwn
--- a/doc/git-annex-registerurl.mdwn
+++ b/doc/git-annex-registerurl.mdwn
@@ -37,6 +37,16 @@
    (Note that for this to be used, you have to explicitly enable batch mode
    with `--batch`)
 
+* `--json`
+
+  Enable JSON output. This is intended to be parsed by programs that use
+  git-annex. Each line of output is a JSON object.
+
+* `--json-error-messages`
+
+  Messages that would normally be output to standard error are included in
+  the json instead.
+
 * Also the [[git-annex-common-options]](1) can be used.
 
 # SEE ALSO
diff --git a/doc/git-annex-unregisterurl.mdwn b/doc/git-annex-unregisterurl.mdwn
--- a/doc/git-annex-unregisterurl.mdwn
+++ b/doc/git-annex-unregisterurl.mdwn
@@ -14,6 +14,9 @@
 Unregistering a key's last web url will make git-annex no longer treat content
 as being present in the web special remote.
 
+Normally the key is a git-annex formatted key. However, if the key cannot be
+parsed as a key, and is a valid url, an URL key is constructed from the url.
+
 # OPTIONS
 
 * `--batch`
@@ -25,6 +28,16 @@
 
    When in batch mode, the input is delimited by nulls instead of the usual
    newlines.
+
+* `--json`
+
+  Enable JSON output. This is intended to be parsed by programs that use
+  git-annex. Each line of output is a JSON object.
+
+* `--json-error-messages`
+
+  Messages that would normally be output to standard error are included in
+  the json instead.
 
 * Also the [[git-annex-common-options]](1) can be used.
 
diff --git a/doc/git-annex.mdwn b/doc/git-annex.mdwn
--- a/doc/git-annex.mdwn
+++ b/doc/git-annex.mdwn
@@ -836,9 +836,7 @@
 
   Set to false to make it an error for commands like "git-annex get"
   to be asked to operate on files that are not checked into git.
-
-  The default is currently true, but is planned to change to false in a
-  release in 2022.
+  (This is the default in recent versions of git-annex.)
 
   Note that, when annex.skipunknown is false, a command like "git-annex get ."
   will fail if no files in the current directory are checked into git,
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.20220127
+Version: 10.20220222
 Cabal-Version: >= 1.10
 License: AGPL-3
 Maintainer: Joey Hess <id@joeyh.name>
