diff --git a/Annex/Export.hs b/Annex/Export.hs
--- a/Annex/Export.hs
+++ b/Annex/Export.hs
@@ -12,9 +12,11 @@
 import Types.Key
 import Types.Remote
 import qualified Git
+import Config
 
 import qualified Data.Map as M
 import Control.Applicative
+import Data.Maybe
 import Prelude
 
 -- An export includes both annexed files and files stored in git.
@@ -40,6 +42,4 @@
 		}
 
 exportTree :: RemoteConfig -> Bool
-exportTree c = case M.lookup "exporttree" c of
-	Just "yes" -> True
-	_ -> False
+exportTree c = fromMaybe False $ yesNo =<< M.lookup "exporttree" c
diff --git a/Annex/Url.hs b/Annex/Url.hs
--- a/Annex/Url.hs
+++ b/Annex/Url.hs
@@ -59,8 +59,8 @@
 			-- it from accessing specific IP addresses.
 			curlopts <- map Param . annexWebOptions <$> Annex.getGitConfig
 			let urldownloader = if null curlopts
-				then U.DownloadWithCurl curlopts
-				else U.DownloadWithConduit
+				then U.DownloadWithConduit
+				else U.DownloadWithCurl curlopts
 			manager <- liftIO $ U.newManager U.managerSettings
 			return (urldownloader, manager)
 		allowedaddrs -> do
diff --git a/Assistant/Threads/Exporter.hs b/Assistant/Threads/Exporter.hs
--- a/Assistant/Threads/Exporter.hs
+++ b/Assistant/Threads/Exporter.hs
@@ -64,6 +64,7 @@
 			Annex.changeState $ \st -> st { Annex.errcounter = 0 }
 			start <- liftIO getCurrentTime
 			void $ Command.Sync.seekExportContent rs
+				=<< join Command.Sync.getCurrBranch
 			-- Look at command error counter to see if the export
 			-- didn't work.
 			failed <- (> 0) <$> Annex.getState Annex.errcounter
diff --git a/Assistant/Upgrade.hs b/Assistant/Upgrade.hs
--- a/Assistant/Upgrade.hs
+++ b/Assistant/Upgrade.hs
@@ -83,7 +83,7 @@
   where
 	go Nothing = debug ["Skipping redundant upgrade"]
 	go (Just dest) = do
-		liftAnnex $ setUrlPresent webUUID k u
+		liftAnnex $ setUrlPresent k u
 		hook <- asIO1 $ distributionDownloadComplete d dest cleanup
 		modifyDaemonStatus_ $ \s -> s
 			{ transferHook = M.insert k hook (transferHook s) }
@@ -100,7 +100,7 @@
 		}
 	cleanup = liftAnnex $ do
 		lockContentForRemoval k removeAnnex
-		setUrlMissing webUUID k u
+		setUrlMissing k u
 		logStatus k InfoMissing
 
 {- Called once the download is done.
diff --git a/CHANGELOG b/CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,26 @@
+git-annex (6.20181011) upstream; urgency=medium
+
+  * sync: Warn when a remote's export is not updated to the current
+    tree because export tracking is not configured.
+  * Improve display when git config download from a http remote fails.
+  * Added annex.jobs setting, which is like using the -J option.
+  * Fix reversion in support of annex.web-options.
+  * rmurl: Fix a case where removing the last url left git-annex thinking
+    content was still present in the web special remote.
+  * SETURLPRESENT, SETURIPRESENT, SETURLMISSING, and SETURIMISSING
+    used to update the presence information of the external special remote
+    that called them; this was not documented behavior and is no longer done.
+  * export: Fix false positive in export conflict detection, that occurred
+    when the same tree was exported by multiple clones.
+  * Fix potential crash in exporttree database due to failure to honor
+    uniqueness constraint.
+  * Fix crash when exporttree is set to a bad value.
+  * Linux standalone: Avoid using bundled cp before envionment is fully set up.
+  * Added arm64 Linux standalone build.
+  * Improved termux installation process.
+
+ -- Joey Hess <id@joeyh.name>  Thu, 11 Oct 2018 13:41:10 -0400
+
 git-annex (6.20180926) upstream; urgency=medium
 
   [ Joey Hess ]
diff --git a/CmdLine/Action.hs b/CmdLine/Action.hs
--- a/CmdLine/Action.hs
+++ b/CmdLine/Action.hs
@@ -78,6 +78,9 @@
 	go NonConcurrent = run
 	run = void $ includeCommandAction a
 
+commandActions :: [CommandStart] -> Annex ()
+commandActions = mapM_ commandAction
+
 {- Waits for any forked off command actions to finish.
  -
  - Merge together the cleanup actions of all the AnnexStates used by
@@ -167,20 +170,32 @@
 {- Do concurrent output when that has been requested. -}
 allowConcurrentOutput :: Annex a -> Annex a
 #ifdef WITH_CONCURRENTOUTPUT
-allowConcurrentOutput a = go =<< Annex.getState Annex.concurrency
+allowConcurrentOutput a = do
+	fromcmdline <- Annex.getState Annex.concurrency
+	fromgitcfg <- annexJobs <$> Annex.getGitConfig
+	case (fromcmdline, fromgitcfg) of
+		(NonConcurrent, NonConcurrent) -> a
+		(Concurrent n, _) -> goconcurrent n
+		(NonConcurrent, Concurrent n) -> do
+			Annex.changeState $ 
+				\c -> c { Annex.concurrency = fromgitcfg }
+			goconcurrent n
   where
-	go NonConcurrent = a
-	go (Concurrent _) = ifM (liftIO concurrentOutputSupported)
-		( Regions.displayConsoleRegions $
-			goconcurrent True
-		, goconcurrent False
-		)
-	goconcurrent b = bracket_ (setup b) cleanup a
-	setup = setconcurrentenabled
+	goconcurrent n = do
+		c <- liftIO getNumCapabilities
+		when (n > c) $
+			liftIO $ setNumCapabilities n
+		ifM (liftIO concurrentOutputSupported)
+			( Regions.displayConsoleRegions $
+				goconcurrent' True
+			, goconcurrent' False
+			)
+	goconcurrent' b = bracket_ (setup b) cleanup a
+	setup = setconcurrentoutputenabled
 	cleanup = do
 		finishCommandActions
-		setconcurrentenabled False
-	setconcurrentenabled b = Annex.changeState $ \s ->
+		setconcurrentoutputenabled False
+	setconcurrentoutputenabled b = Annex.changeState $ \s ->
 		s { Annex.output = (Annex.output s) { concurrentOutputEnabled = b } }
 #else
 allowConcurrentOutput = id
diff --git a/CmdLine/GitAnnex/Options.hs b/CmdLine/GitAnnex/Options.hs
--- a/CmdLine/GitAnnex/Options.hs
+++ b/CmdLine/GitAnnex/Options.hs
@@ -13,7 +13,6 @@
 #if ! MIN_VERSION_optparse_applicative(0,14,1)
 import Options.Applicative.Builder.Internal
 #endif
-import Control.Concurrent
 import qualified Data.Map as M
 
 import Annex.Common
@@ -370,11 +369,7 @@
 			)
 	]
   where
-	set n = do
-		Annex.changeState $ \s -> s { Annex.concurrency = Concurrent n }
-		c <- liftIO getNumCapabilities
-		when (n > c) $
-			liftIO $ setNumCapabilities n
+	set n = Annex.changeState $ \s -> s { Annex.concurrency = Concurrent n }
 
 timeLimitOption :: [GlobalOption]
 timeLimitOption = 
diff --git a/CmdLine/Seek.hs b/CmdLine/Seek.hs
--- a/CmdLine/Seek.hs
+++ b/CmdLine/Seek.hs
@@ -22,7 +22,6 @@
 import Git.FilePath
 import qualified Limit
 import CmdLine.GitAnnex.Options
-import CmdLine.Action
 import Logs.Location
 import Logs.Unused
 import Types.Transfer
@@ -34,11 +33,11 @@
 import Annex.InodeSentinal
 import qualified Database.Keys
 
-withFilesInGit :: (FilePath -> CommandStart) -> [WorkTreeItem] -> CommandSeek
+withFilesInGit :: (FilePath -> CommandSeek) -> [WorkTreeItem] -> CommandSeek
 withFilesInGit a l = seekActions $ prepFiltered a $
 	seekHelper LsFiles.inRepo l
 
-withFilesInGitNonRecursive :: String -> (FilePath -> CommandStart) -> [WorkTreeItem] -> CommandSeek
+withFilesInGitNonRecursive :: String -> (FilePath -> CommandSeek) -> [WorkTreeItem] -> CommandSeek
 withFilesInGitNonRecursive needforce a l = ifM (Annex.getState Annex.force)
 	( withFilesInGit a l
 	, if null l
@@ -58,7 +57,7 @@
 				getfiles c ps
 			_ -> giveup needforce
 
-withFilesNotInGit :: Bool -> (FilePath -> CommandStart) -> [WorkTreeItem] -> CommandSeek
+withFilesNotInGit :: Bool -> (FilePath -> CommandSeek) -> [WorkTreeItem] -> CommandSeek
 withFilesNotInGit skipdotfiles a l
 	| skipdotfiles = do
 		{- dotfiles are not acted on unless explicitly listed -}
@@ -78,7 +77,7 @@
 	go fs = seekActions $ prepFiltered a $
 		return $ concat $ segmentPaths (map (\(WorkTreeItem f) -> f) l) fs
 
-withFilesInRefs :: (FilePath -> Key -> CommandStart) -> [Git.Ref] -> CommandSeek
+withFilesInRefs :: ((FilePath, Key) -> CommandSeek) -> [Git.Ref] -> CommandSeek
 withFilesInRefs a = mapM_ go
   where
 	go r = do	
@@ -89,16 +88,17 @@
 			catKey (LsTree.sha i) >>= \case
 				Nothing -> noop
 				Just k -> whenM (matcher $ MatchingKey k) $
-					commandAction $ a f k
+					a (f, k)
 		liftIO $ void cleanup
 
-withPathContents :: ((FilePath, FilePath) -> CommandStart) -> CmdParams -> CommandSeek
+withPathContents :: ((FilePath, FilePath) -> CommandSeek) -> CmdParams -> CommandSeek
 withPathContents a params = do
 	matcher <- Limit.getMatcher
 	forM_ params $ \p -> do
 		fs <- liftIO $ get p
-		forM fs $ \f -> whenM (checkmatch matcher f) $
-			commandAction (a f)	
+		forM fs $ \f ->
+			whenM (checkmatch matcher f) $
+				a f
   where
 	get p = ifM (isDirectory <$> getFileStatus p)
 		( map (\f -> (f, makeRelative (parentDir p) f))
@@ -110,24 +110,24 @@
 		, matchFile = relf
 		}
 
-withWords :: ([String] -> CommandStart) -> CmdParams -> CommandSeek
+withWords :: ([String] -> CommandSeek) -> CmdParams -> CommandSeek
 withWords a params = seekActions $ return [a params]
 
-withStrings :: (String -> CommandStart) -> CmdParams -> CommandSeek
+withStrings :: (String -> CommandSeek) -> CmdParams -> CommandSeek
 withStrings a params = seekActions $ return $ map a params
 
-withPairs :: ((String, String) -> CommandStart) -> CmdParams -> CommandSeek
+withPairs :: ((String, String) -> CommandSeek) -> CmdParams -> CommandSeek
 withPairs a params = seekActions $ return $ map a $ pairs [] params
   where
 	pairs c [] = reverse c
 	pairs c (x:y:xs) = pairs ((x,y):c) xs
 	pairs _ _ = giveup "expected pairs"
 
-withFilesToBeCommitted :: (FilePath -> CommandStart) -> [WorkTreeItem] -> CommandSeek
+withFilesToBeCommitted :: (FilePath -> CommandSeek) -> [WorkTreeItem] -> CommandSeek
 withFilesToBeCommitted a l = seekActions $ prepFiltered a $
 	seekHelper LsFiles.stagedNotDeleted l
 
-withFilesOldUnlocked :: (FilePath -> CommandStart) -> [WorkTreeItem] -> CommandSeek
+withFilesOldUnlocked :: (FilePath -> CommandSeek) -> [WorkTreeItem] -> CommandSeek
 withFilesOldUnlocked = withFilesOldUnlocked' LsFiles.typeChanged
 
 {- Unlocked files before v6 have changed type from a symlink to a regular file.
@@ -135,7 +135,7 @@
  - Furthermore, unlocked files used to be a git-annex symlink,
  - not some other sort of symlink.
  -}
-withFilesOldUnlocked' :: ([FilePath] -> Git.Repo -> IO ([FilePath], IO Bool)) -> (FilePath -> CommandStart) -> [WorkTreeItem] -> CommandSeek
+withFilesOldUnlocked' :: ([FilePath] -> Git.Repo -> IO ([FilePath], IO Bool)) -> (FilePath -> CommandSeek) -> [WorkTreeItem] -> CommandSeek
 withFilesOldUnlocked' typechanged a l = seekActions $
 	prepFiltered a unlockedfiles
   where
@@ -145,12 +145,12 @@
 isOldUnlocked f = liftIO (notSymlink f) <&&> 
 	(isJust <$> catKeyFile f <||> isJust <$> catKeyFileHEAD f)
 
-withFilesOldUnlockedToBeCommitted :: (FilePath -> CommandStart) -> [WorkTreeItem] -> CommandSeek
+withFilesOldUnlockedToBeCommitted :: (FilePath -> CommandSeek) -> [WorkTreeItem] -> CommandSeek
 withFilesOldUnlockedToBeCommitted = withFilesOldUnlocked' LsFiles.typeChangedStaged
 
 {- v6 unlocked pointer files that are staged, and whose content has not been
  - modified-}
-withUnmodifiedUnlockedPointers :: (FilePath -> CommandStart) -> [WorkTreeItem] -> CommandSeek
+withUnmodifiedUnlockedPointers :: (FilePath -> CommandSeek) -> [WorkTreeItem] -> CommandSeek
 withUnmodifiedUnlockedPointers a l = seekActions $
 	prepFiltered a unlockedfiles
   where
@@ -163,17 +163,17 @@
 	Just k -> sameInodeCache f =<< Database.Keys.getInodeCaches k
 
 {- Finds files that may be modified. -}
-withFilesMaybeModified :: (FilePath -> CommandStart) -> [WorkTreeItem] -> CommandSeek
+withFilesMaybeModified :: (FilePath -> CommandSeek) -> [WorkTreeItem] -> CommandSeek
 withFilesMaybeModified a params = seekActions $
 	prepFiltered a $ seekHelper LsFiles.modified params
 
-withKeys :: (Key -> CommandStart) -> CmdParams -> CommandSeek
+withKeys :: (Key -> CommandSeek) -> CmdParams -> CommandSeek
 withKeys a l = seekActions $ return $ map (a . parse) l
   where
 	parse p = fromMaybe (giveup "bad key") $ file2key p
 
-withNothing :: CommandStart -> CmdParams -> CommandSeek
-withNothing a [] = seekActions $ return [a]
+withNothing :: CommandSeek -> CmdParams -> CommandSeek
+withNothing a [] = a
 withNothing _ _ = giveup "This command takes no parameters."
 
 {- Handles the --all, --branch, --unused, --failed, --key, and
@@ -183,11 +183,12 @@
  - In a bare repo, --all is the default.
  -
  - Otherwise falls back to a regular CommandSeek action on
- - whatever params were passed. -}
+ - whatever params were passed.
+ -}
 withKeyOptions 
 	:: Maybe KeyOptions
 	-> Bool
-	-> (Key -> ActionItem -> CommandStart)
+	-> ((Key, ActionItem) -> CommandSeek)
 	-> ([WorkTreeItem] -> CommandSeek)
 	-> [WorkTreeItem]
 	-> CommandSeek
@@ -195,14 +196,14 @@
   where
 	mkkeyaction = do
 		matcher <- Limit.getMatcher
-		return $ \k i ->
-			whenM (matcher $ MatchingKey k) $
-				commandAction $ keyaction k i
+		return $ \v ->
+			whenM (matcher $ MatchingKey $ fst v) $
+				keyaction v
 
 withKeyOptions' 
 	:: Maybe KeyOptions
 	-> Bool
-	-> Annex (Key -> ActionItem -> Annex ())
+	-> Annex ((Key, ActionItem) -> Annex ())
 	-> ([WorkTreeItem] -> CommandSeek)
 	-> [WorkTreeItem]
 	-> CommandSeek
@@ -231,14 +232,14 @@
 		keyaction <- mkkeyaction
 		ks <- getks
 		forM_ ks $ checker >=> maybe noop 
-			(\k -> keyaction k (mkActionItem k))
+			(\k -> keyaction (k, mkActionItem k))
 	runbranchkeys bs = do
 		keyaction <- mkkeyaction
 		forM_ bs $ \b -> do
 			(l, cleanup) <- inRepo $ LsTree.lsTree b
 			forM_ l $ \i -> do
 				let bfp = mkActionItem $ BranchFilePath b (LsTree.file i)
-				maybe noop (\k -> keyaction k bfp)
+				maybe noop (\k -> keyaction (k, bfp))
 					=<< catKey (LsTree.sha i)
 			unlessM (liftIO cleanup) $
 				error ("git ls-tree " ++ Git.fromRef b ++ " failed")
@@ -247,18 +248,17 @@
 		rs <- remoteList
 		ts <- concat <$> mapM (getFailedTransfers . Remote.uuid) rs
 		forM_ ts $ \(t, i) ->
-			keyaction (transferKey t) (mkActionItem (t, i))
+			keyaction (transferKey t, mkActionItem (t, i))
 
-prepFiltered :: (FilePath -> CommandStart) -> Annex [FilePath] -> Annex [CommandStart]
+prepFiltered :: (FilePath -> CommandSeek) -> Annex [FilePath] -> Annex [CommandSeek]
 prepFiltered a fs = do
 	matcher <- Limit.getMatcher
 	map (process matcher) <$> fs
   where
-	process matcher f = ifM (matcher $ MatchingFile $ FileInfo f f)
-		( a f , return Nothing )
+	process matcher f = whenM (matcher $ MatchingFile $ FileInfo f f) $ a f
 
-seekActions :: Annex [CommandStart] -> Annex ()
-seekActions gen = mapM_ commandAction =<< gen
+seekActions :: Annex [CommandSeek] -> Annex ()
+seekActions gen = sequence_ =<< gen
 
 seekHelper :: ([FilePath] -> Git.Repo -> IO ([FilePath], IO Bool)) -> [WorkTreeItem] -> Annex [FilePath]
 seekHelper a l = inRepo $ \g ->
diff --git a/Command/Add.hs b/Command/Add.hs
--- a/Command/Add.hs
+++ b/Command/Add.hs
@@ -65,7 +65,7 @@
 			| otherwise -> batchFilesMatching fmt gofile
 		NoBatch -> do
 			l <- workTreeItems (addThese o)
-			let go a = a gofile l
+			let go a = a (commandAction . gofile) l
 			unless (updateOnly o) $
 				go (withFilesNotInGit (not $ includeDotFiles o))
 			go withFilesMaybeModified
diff --git a/Command/AddUrl.hs b/Command/AddUrl.hs
--- a/Command/AddUrl.hs
+++ b/Command/AddUrl.hs
@@ -233,7 +233,8 @@
 			(exists, samesize, url') <- checkexistssize key
 			if exists && (samesize || relaxedOption (downloadOptions o))
 				then do
-					setUrlPresent u key url'
+					setUrlPresent key url'
+					logChange key u InfoPresent
 					next $ return True
 				else do
 					warning $ "while adding a new url to an already annexed file, " ++ if exists
@@ -397,7 +398,8 @@
   where
 	go = do
 		maybeShowJSON $ JSONChunk [("key", key2file key)]
-		setUrlPresent u key url
+		setUrlPresent key url
+		logChange key u InfoPresent
 		ifM (addAnnexedFile file key mtmp)
 			( do
 				when (isJust mtmp) $
diff --git a/Command/Commit.hs b/Command/Commit.hs
--- a/Command/Commit.hs
+++ b/Command/Commit.hs
@@ -17,7 +17,7 @@
 	paramNothing (withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withNothing start
+seek = withNothing (commandAction start)
 
 start :: CommandStart
 start = next $ next $ do
diff --git a/Command/ConfigList.hs b/Command/ConfigList.hs
--- a/Command/ConfigList.hs
+++ b/Command/ConfigList.hs
@@ -23,7 +23,7 @@
 		paramNothing (withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withNothing start
+seek = withNothing (commandAction start)
 
 start :: CommandStart
 start = do
diff --git a/Command/Copy.hs b/Command/Copy.hs
--- a/Command/Copy.hs
+++ b/Command/Copy.hs
@@ -50,8 +50,8 @@
 		Batch fmt -> batchFilesMatching fmt go
 		NoBatch -> withKeyOptions
 			(keyOptions o) (autoMode o)
-			(Command.Move.startKey (fromToOptions o) Command.Move.RemoveNever)
-			(withFilesInGit go)
+			(commandAction . Command.Move.startKey (fromToOptions o) Command.Move.RemoveNever)
+			(withFilesInGit $ commandAction . go)
 			=<< workTreeItems (copyFiles o)
 
 {- A copy is just a move that does not delete the source file.
diff --git a/Command/Dead.hs b/Command/Dead.hs
--- a/Command/Dead.hs
+++ b/Command/Dead.hs
@@ -29,7 +29,7 @@
 
 seek :: DeadOptions -> CommandSeek
 seek (DeadRemotes rs) = trustCommand "dead" DeadTrusted rs
-seek (DeadKeys ks) = seekActions $ pure $ map startKey ks
+seek (DeadKeys ks) = commandActions $ map startKey ks
 
 startKey :: Key -> CommandStart
 startKey key = do
diff --git a/Command/Describe.hs b/Command/Describe.hs
--- a/Command/Describe.hs
+++ b/Command/Describe.hs
@@ -18,7 +18,7 @@
 	(withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withWords start
+seek = withWords (commandAction . start)
 
 start :: [String] -> CommandStart
 start (name:description) = do
diff --git a/Command/DiffDriver.hs b/Command/DiffDriver.hs
--- a/Command/DiffDriver.hs
+++ b/Command/DiffDriver.hs
@@ -19,7 +19,7 @@
 		("-- cmd --") (withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withWords start
+seek = withWords (commandAction . start)
 
 start :: [String] -> CommandStart
 start opts = do
diff --git a/Command/Direct.hs b/Command/Direct.hs
--- a/Command/Direct.hs
+++ b/Command/Direct.hs
@@ -21,7 +21,7 @@
 		paramNothing (withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withNothing start
+seek = withNothing (commandAction start)
 
 start :: CommandStart
 start = ifM versionSupportsDirectMode
diff --git a/Command/Drop.hs b/Command/Drop.hs
--- a/Command/Drop.hs
+++ b/Command/Drop.hs
@@ -56,8 +56,8 @@
 	case batchOption o of
 		Batch fmt -> batchFilesMatching fmt go
 		NoBatch -> withKeyOptions (keyOptions o) (autoMode o)
-			(startKeys o)
-			(withFilesInGit go)
+			(commandAction . startKeys o)
+			(withFilesInGit (commandAction . go))
 			=<< workTreeItems (dropFiles o)
   where
 	go = whenAnnexed $ start o
@@ -84,8 +84,8 @@
 			| autoMode o = wantDrop False (Remote.uuid <$> from) (Just key) afile
 			| otherwise = return True
 
-startKeys :: DropOptions -> Key -> ActionItem -> CommandStart
-startKeys o key = start' o key (AssociatedFile Nothing)
+startKeys :: DropOptions -> (Key, ActionItem) -> CommandStart
+startKeys o (key, ai) = start' o key (AssociatedFile Nothing) ai
 
 startLocal :: AssociatedFile -> ActionItem -> NumCopies -> Key -> [VerifiedCopy] -> CommandStart
 startLocal afile ai numcopies key preverified = stopUnless (inAnnex key) $ do
diff --git a/Command/DropKey.hs b/Command/DropKey.hs
--- a/Command/DropKey.hs
+++ b/Command/DropKey.hs
@@ -33,7 +33,7 @@
 seek o = do
 	unlessM (Annex.getState Annex.force) $
 		giveup "dropkey can cause data loss; use --force if you're sure you want to do this"
-	withKeys start (toDrop o)
+	withKeys (commandAction . start) (toDrop o)
 	case batchOption o of
 		Batch fmt -> batchInput fmt parsekey $ batchCommandAction . start
 		NoBatch -> noop
diff --git a/Command/EnableRemote.hs b/Command/EnableRemote.hs
--- a/Command/EnableRemote.hs
+++ b/Command/EnableRemote.hs
@@ -32,7 +32,7 @@
 	(withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withWords start
+seek = withWords (commandAction . start)
 
 start :: [String] -> CommandStart
 start [] = unknownNameError "Specify the remote to enable."
diff --git a/Command/EnableTor.hs b/Command/EnableTor.hs
--- a/Command/EnableTor.hs
+++ b/Command/EnableTor.hs
@@ -36,7 +36,7 @@
 		"uid" (withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withWords start
+seek = withWords (commandAction . start)
 
 -- This runs as root, so avoid making any commits or initializing
 -- git-annex, or doing other things that create root-owned files.
diff --git a/Command/Expire.hs b/Command/Expire.hs
--- a/Command/Expire.hs
+++ b/Command/Expire.hs
@@ -53,7 +53,7 @@
 	u <- getUUID
 	us <- filter (/= u) . M.keys <$> uuidMap
 	descs <- uuidMap
-	seekActions $ pure $ map (start expire (noActOption o) actlog descs) us
+	commandActions $ map (start expire (noActOption o) actlog descs) us
 
 start :: Expire -> Bool -> Log Activity -> M.Map UUID String -> UUID -> CommandStart
 start (Expire expire) noact actlog descs u =
diff --git a/Command/Export.hs b/Command/Export.hs
--- a/Command/Export.hs
+++ b/Command/Export.hs
@@ -99,10 +99,12 @@
 	-- the next block of code below may have renamed some files to
 	-- temp files. Diff from the incomplete tree to the new tree,
 	-- and delete any temp files that the new tree can't use.
+	let recover diff = commandAction $
+		startRecoverIncomplete r ea db
+			(Git.DiffTree.srcsha diff)
+			(Git.DiffTree.file diff)
 	forM_ (concatMap incompleteExportedTreeish old) $ \incomplete ->
-		mapdiff (\diff -> startRecoverIncomplete r ea db (Git.DiffTree.srcsha diff) (Git.DiffTree.file diff))
-			incomplete
-			new
+		mapdiff recover incomplete new
 
 	-- Diff the old and new trees, and delete or rename to new name all
 	-- changed files in the export. After this, every file that remains
@@ -111,17 +113,18 @@
 	-- When there was an export conflict, this resolves it.
 	--
 	-- The ExportTree is also updated here to reflect the new tree.
-	case map exportedTreeish old of
+	case nub (map exportedTreeish old) of
 		[] -> updateExportTree db emptyTree new
 		[oldtreesha] -> do
 			diffmap <- mkDiffMap oldtreesha new db
-			let seekdiffmap a = seekActions $ pure $ map a (M.toList diffmap)
+			let seekdiffmap a = commandActions $ 
+				map a (M.toList diffmap)
 			-- Rename old files to temp, or delete.
 			seekdiffmap $ \(ek, (moldf, mnewf)) -> do
 				case (moldf, mnewf) of
 					(Just oldf, Just _newf) ->
 						startMoveToTempName r ea db oldf ek
-					(Just oldf, Nothing) -> 
+					(Just oldf, Nothing) ->
 						startUnexport' r ea db oldf ek
 					_ -> stop
 			-- Rename from temp to new files.
@@ -144,7 +147,7 @@
 				-- Don't rename to temp, because the
 				-- content is unknown; delete instead.
 				mapdiff
-					(\diff -> startUnexport r ea db (Git.DiffTree.file diff) (unexportboth diff))
+					(\diff -> commandAction $ startUnexport r ea db (Git.DiffTree.file diff) (unexportboth diff))
 					oldtreesha new
 			updateExportTree db emptyTree new
 	liftIO $ recordExportTreeCurrent db new
@@ -194,7 +197,7 @@
 fillExport r ea db new = do
 	(l, cleanup) <- inRepo $ Git.LsTree.lsTree new
 	cvar <- liftIO $ newMVar False
-	seekActions $ pure $ map (startExport r ea db cvar) l
+	commandActions $ map (startExport r ea db cvar) l
 	void $ liftIO $ cleanup
 	liftIO $ takeMVar cvar
 
diff --git a/Command/Find.hs b/Command/Find.hs
--- a/Command/Find.hs
+++ b/Command/Find.hs
@@ -50,7 +50,8 @@
 
 seek :: FindOptions -> CommandSeek
 seek o = case batchOption o of
-	NoBatch -> withFilesInGit go =<< workTreeItems (findThese o)
+	NoBatch -> withFilesInGit (commandAction . go)
+		=<< workTreeItems (findThese o)
 	Batch fmt -> batchFilesMatching fmt go
   where
 	go = whenAnnexed $ start o
diff --git a/Command/FindRef.hs b/Command/FindRef.hs
--- a/Command/FindRef.hs
+++ b/Command/FindRef.hs
@@ -18,4 +18,5 @@
 		paramRef (seek <$$> Find.optParser)
 
 seek :: Find.FindOptions -> CommandSeek
-seek o = Find.start o `withFilesInRefs` (map Git.Ref $ Find.findThese o)
+seek o = (commandAction . uncurry (Find.start o))
+	`withFilesInRefs` (map Git.Ref $ Find.findThese o)
diff --git a/Command/Fix.hs b/Command/Fix.hs
--- a/Command/Fix.hs
+++ b/Command/Fix.hs
@@ -34,8 +34,9 @@
 		( return FixAll
 		, return FixSymlinks
 		)
-	l <- workTreeItems ps
-	flip withFilesInGit l $ whenAnnexed $ start fixwhat
+	withFilesInGit
+		(commandAction . (whenAnnexed $ start fixwhat))
+		=<< workTreeItems ps
 
 data FixWhat = FixSymlinks | FixAll
 
diff --git a/Command/FromKey.hs b/Command/FromKey.hs
--- a/Command/FromKey.hs
+++ b/Command/FromKey.hs
@@ -35,12 +35,12 @@
 
 seek :: FromKeyOptions -> CommandSeek
 seek o = case (batchOption o, keyFilePairs o) of
-	(Batch fmt, _) -> withNothing (startMass fmt) []
+	(Batch fmt, _) -> commandAction $ startMass fmt
 	-- older way of enabling batch input, does not support BatchNull
-	(NoBatch, []) -> withNothing (startMass BatchLine) []
+	(NoBatch, []) -> commandAction $ startMass BatchLine
 	(NoBatch, ps) -> do
 		force <- Annex.getState Annex.force
-		withPairs (start force) ps
+		withPairs (commandAction . start force) ps
 
 start :: Bool -> (String, FilePath) -> CommandStart
 start force (keyname, file) = do
diff --git a/Command/Fsck.hs b/Command/Fsck.hs
--- a/Command/Fsck.hs
+++ b/Command/Fsck.hs
@@ -94,8 +94,8 @@
 	checkDeadRepo u
 	i <- prepIncremental u (incrementalOpt o)
 	withKeyOptions (keyOptions o) False
-		(\k ai -> startKey from i k ai =<< getNumCopies)
-		(withFilesInGit $ whenAnnexed $ start from i)
+		(\kai -> commandAction . startKey from i kai =<< getNumCopies)
+		(withFilesInGit $ commandAction . (whenAnnexed (start from i)))
 		=<< workTreeItems (fsckFiles o)
 	cleanupIncremental i
 	void $ tryIO $ recordActivity Fsck u
@@ -183,8 +183,8 @@
 		)
 	dummymeter _ = noop
 
-startKey :: Maybe Remote -> Incremental -> Key -> ActionItem -> NumCopies -> CommandStart
-startKey from inc key ai numcopies =
+startKey :: Maybe Remote -> Incremental -> (Key, ActionItem) -> NumCopies -> CommandStart
+startKey from inc (key, ai) numcopies =
 	case Backend.maybeLookupBackendVariety (keyVariety key) of
 		Nothing -> stop
 		Just backend -> runFsck inc ai key $
diff --git a/Command/FuzzTest.hs b/Command/FuzzTest.hs
--- a/Command/FuzzTest.hs
+++ b/Command/FuzzTest.hs
@@ -26,7 +26,7 @@
 		paramNothing (withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withNothing start
+seek = withNothing (commandAction start)
 
 start :: CommandStart
 start = do
diff --git a/Command/GCryptSetup.hs b/Command/GCryptSetup.hs
--- a/Command/GCryptSetup.hs
+++ b/Command/GCryptSetup.hs
@@ -19,7 +19,7 @@
 		paramValue (withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withStrings start
+seek = withStrings (commandAction . start)
 
 start :: String -> CommandStart
 start gcryptid = next $ next $ do
diff --git a/Command/Get.hs b/Command/Get.hs
--- a/Command/Get.hs
+++ b/Command/Get.hs
@@ -44,8 +44,8 @@
 	case batchOption o of
 		Batch fmt -> batchFilesMatching fmt go
 		NoBatch -> withKeyOptions (keyOptions o) (autoMode o)
-			(startKeys from)
-			(withFilesInGit go)
+			(commandAction . startKeys from)
+			(withFilesInGit (commandAction . go))
 			=<< workTreeItems (getFiles o)
 
 start :: GetOptions -> Maybe Remote -> FilePath -> Key -> CommandStart
@@ -57,8 +57,8 @@
 			<||> wantGet False (Just key) afile
 		| otherwise = return True
 
-startKeys :: Maybe Remote -> Key -> ActionItem -> CommandStart
-startKeys from key ai = checkFailedTransferDirection ai Download $
+startKeys :: Maybe Remote -> (Key, ActionItem) -> CommandStart
+startKeys from (key, ai) = checkFailedTransferDirection ai Download $
 	start' (return True) from key (AssociatedFile Nothing) ai
 
 start' :: Annex Bool -> Maybe Remote -> Key -> AssociatedFile -> ActionItem -> CommandStart
diff --git a/Command/Group.hs b/Command/Group.hs
--- a/Command/Group.hs
+++ b/Command/Group.hs
@@ -19,7 +19,7 @@
 	(paramPair paramRemote paramDesc) (withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withWords start
+seek = withWords (commandAction . start)
 
 start :: [String] -> CommandStart
 start (name:g:[]) = do
diff --git a/Command/GroupWanted.hs b/Command/GroupWanted.hs
--- a/Command/GroupWanted.hs
+++ b/Command/GroupWanted.hs
@@ -18,7 +18,7 @@
 	(withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withWords start
+seek = withWords (commandAction . start)
 
 start :: [String] -> CommandStart
 start (g:[]) = next $ performGet groupPreferredContentMapRaw g
diff --git a/Command/Help.hs b/Command/Help.hs
--- a/Command/Help.hs
+++ b/Command/Help.hs
@@ -27,7 +27,7 @@
 	parseparams = withParams
 
 seek :: CmdParams -> CommandSeek
-seek = withWords start
+seek = withWords (commandAction . start)
 
 start :: [String] -> CommandStart
 start params = do
diff --git a/Command/Import.hs b/Command/Import.hs
--- a/Command/Import.hs
+++ b/Command/Import.hs
@@ -73,7 +73,8 @@
 	unless (null inrepops) $ do
 		giveup $ "cannot import files from inside the working tree (use git annex add instead): " ++ unwords inrepops
 	largematcher <- largeFilesMatcher
-	withPathContents (start largematcher (duplicateMode o)) (importFiles o)
+	(commandAction . start largematcher (duplicateMode o))
+		`withPathContents` importFiles o
 
 start :: GetFileMatcher -> DuplicateMode -> (FilePath, FilePath) -> CommandStart
 start largematcher mode (srcfile, destfile) =
diff --git a/Command/ImportFeed.hs b/Command/ImportFeed.hs
--- a/Command/ImportFeed.hs
+++ b/Command/ImportFeed.hs
@@ -67,7 +67,7 @@
 seek :: ImportFeedOptions -> CommandSeek
 seek o = do
 	cache <- getCache (templateOption o)
-	withStrings (start o cache) (feedUrls o)
+	withStrings (commandAction . start o cache) (feedUrls o)
 
 start :: ImportFeedOptions -> Cache -> URLString -> CommandStart
 start opts cache url = do
diff --git a/Command/InAnnex.hs b/Command/InAnnex.hs
--- a/Command/InAnnex.hs
+++ b/Command/InAnnex.hs
@@ -18,7 +18,7 @@
 		(withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withKeys start
+seek = withKeys (commandAction . start)
 
 start :: Key -> CommandStart
 start key = inAnnexSafe key >>= dispatch
diff --git a/Command/Indirect.hs b/Command/Indirect.hs
--- a/Command/Indirect.hs
+++ b/Command/Indirect.hs
@@ -27,7 +27,7 @@
 		paramNothing (withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withNothing start
+seek = withNothing (commandAction start)
 
 start :: CommandStart
 start = ifM isDirect
diff --git a/Command/Info.hs b/Command/Info.hs
--- a/Command/Info.hs
+++ b/Command/Info.hs
@@ -132,7 +132,7 @@
 
 seek :: InfoOptions -> CommandSeek
 seek o = case batchOption o of
-	NoBatch -> withWords (start o) (infoFor o)
+	NoBatch -> withWords (commandAction . start o) (infoFor o)
 	Batch fmt -> batchInput fmt Right (itemInfo o)
 
 start :: InfoOptions -> [String] -> CommandStart
diff --git a/Command/InitRemote.hs b/Command/InitRemote.hs
--- a/Command/InitRemote.hs
+++ b/Command/InitRemote.hs
@@ -24,7 +24,7 @@
 	(withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withWords start
+seek = withWords (commandAction . start)
 
 start :: [String] -> CommandStart
 start [] = giveup "Specify a name for the remote."
diff --git a/Command/Inprogress.hs b/Command/Inprogress.hs
--- a/Command/Inprogress.hs
+++ b/Command/Inprogress.hs
@@ -38,7 +38,8 @@
 		then forM_ ts $ commandAction . start'
 		else do
 			let s = S.fromList ts
-			withFilesInGit (whenAnnexed (start s))
+			withFilesInGit
+				(commandAction . (whenAnnexed (start s)))
 				=<< workTreeItems (inprogressFiles o)
 
 start :: S.Set Key -> FilePath -> Key -> CommandStart
diff --git a/Command/List.hs b/Command/List.hs
--- a/Command/List.hs
+++ b/Command/List.hs
@@ -44,7 +44,8 @@
 seek o = do
 	list <- getList o
 	printHeader list
-	withFilesInGit (whenAnnexed $ start list)
+	withFilesInGit
+		(commandAction . (whenAnnexed $ start list))
 		=<< workTreeItems (listThese o)
 
 getList :: ListOptions -> Annex [(UUID, RemoteName, TrustLevel)]
diff --git a/Command/Lock.hs b/Command/Lock.hs
--- a/Command/Lock.hs
+++ b/Command/Lock.hs
@@ -32,10 +32,10 @@
 seek ps = do
 	l <- workTreeItems ps
 	ifM versionSupportsUnlockedPointers
-		( withFilesInGit (whenAnnexed startNew) l
+		( withFilesInGit (commandAction . (whenAnnexed startNew)) l
 		, do
-			withFilesOldUnlocked startOld l
-			withFilesOldUnlockedToBeCommitted startOld l
+			withFilesOldUnlocked (commandAction . startOld) l
+			withFilesOldUnlockedToBeCommitted (commandAction . startOld) l
 		)
 
 startNew :: FilePath -> Key -> CommandStart
diff --git a/Command/LockContent.hs b/Command/LockContent.hs
--- a/Command/LockContent.hs
+++ b/Command/LockContent.hs
@@ -20,7 +20,7 @@
 		(withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withWords start
+seek = withWords (commandAction . start)
 
 -- First, lock the content, then print out "OK". 
 -- Wait for the caller to send a line before dropping the lock.
diff --git a/Command/Log.hs b/Command/Log.hs
--- a/Command/Log.hs
+++ b/Command/Log.hs
@@ -91,7 +91,8 @@
 	zone <- liftIO getCurrentTimeZone
 	let outputter = mkOutputter m zone o
 	case (logFiles o, allOption o) of
-		(fs, False) -> withFilesInGit (whenAnnexed $ start o outputter) 
+		(fs, False) -> withFilesInGit
+			(commandAction . (whenAnnexed $ start o outputter)) 
 			=<< workTreeItems fs
 		([], True) -> commandAction (startAll o outputter)
 		(_, True) -> giveup "Cannot specify both files and --all"
diff --git a/Command/Map.hs b/Command/Map.hs
--- a/Command/Map.hs
+++ b/Command/Map.hs
@@ -37,7 +37,7 @@
 		paramNothing (withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withNothing start
+seek = withNothing (commandAction start)
 
 start :: CommandStart
 start = do
diff --git a/Command/MetaData.hs b/Command/MetaData.hs
--- a/Command/MetaData.hs
+++ b/Command/MetaData.hs
@@ -80,8 +80,8 @@
 			Set _ -> withFilesInGitNonRecursive
 				"Not recursively setting metadata. Use --force to do that."
 		withKeyOptions (keyOptions o) False
-			(startKeys c o)
-			(seeker $ whenAnnexed $ start c o)
+			(commandAction . startKeys c o)
+			(seeker (commandAction . (whenAnnexed (start c o))))
 			=<< workTreeItems (forFiles o)
 	Batch fmt -> withMessageState $ \s -> case outputType s of
 		JSONOutput _ -> ifM limited
@@ -92,12 +92,12 @@
 		_ -> giveup "--batch is currently only supported in --json mode"
 
 start :: VectorClock -> MetaDataOptions -> FilePath -> Key -> CommandStart
-start c o file k = startKeys c o k (mkActionItem afile)
+start c o file k = startKeys c o (k, mkActionItem afile)
   where
 	afile = AssociatedFile (Just file)
 
-startKeys :: VectorClock -> MetaDataOptions -> Key -> ActionItem -> CommandStart
-startKeys c o k ai = case getSet o of
+startKeys :: VectorClock -> MetaDataOptions -> (Key, ActionItem) -> CommandStart
+startKeys c o (k, ai) = case getSet o of
 	Get f -> do
 		l <- S.toList . currentMetaDataValues f <$> getCurrentMetaData k
 		liftIO $ forM_ l $
diff --git a/Command/Migrate.hs b/Command/Migrate.hs
--- a/Command/Migrate.hs
+++ b/Command/Migrate.hs
@@ -17,7 +17,6 @@
 import qualified Annex
 import Logs.MetaData
 import Logs.Web
-import qualified Remote
 
 cmd :: Command
 cmd = notDirect $ withGlobalOptions [annexedMatchingOptions] $
@@ -26,7 +25,7 @@
 		paramPaths (withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek ps = withFilesInGit (whenAnnexed start) =<< workTreeItems ps
+seek = withFilesInGit (commandAction . (whenAnnexed start)) <=< workTreeItems
 
 start :: FilePath -> Key -> CommandStart
 start file key = do
@@ -78,9 +77,8 @@
 			-- If the old key had some associated urls, record them for
 			-- the new key as well.
 			urls <- getUrls oldkey
-			forM_ urls $ \url -> do
-				r <- Remote.claimingUrl url
-				setUrlPresent (Remote.uuid r) newkey url
+			forM_ urls $ \url ->
+				setUrlPresent newkey url
 			next $ Command.ReKey.cleanup file oldkey newkey
 		, error "failed"
 		)
diff --git a/Command/Mirror.hs b/Command/Mirror.hs
--- a/Command/Mirror.hs
+++ b/Command/Mirror.hs
@@ -43,17 +43,17 @@
 seek :: MirrorOptions -> CommandSeek
 seek o = allowConcurrentOutput $ 
 	withKeyOptions (keyOptions o) False
-		(startKey o (AssociatedFile Nothing))
-		(withFilesInGit $ whenAnnexed $ start o)
+		(commandAction . startKey o (AssociatedFile Nothing))
+		(withFilesInGit (commandAction . (whenAnnexed $ start o)))
 		=<< workTreeItems (mirrorFiles o)
 
 start :: MirrorOptions -> FilePath -> Key -> CommandStart
-start o file k = startKey o afile k (mkActionItem afile)
+start o file k = startKey o afile (k, mkActionItem afile)
   where
 	afile = AssociatedFile (Just file)
 
-startKey :: MirrorOptions -> AssociatedFile -> Key -> ActionItem -> CommandStart
-startKey o afile key ai = onlyActionOn key $ case fromToOptions o of
+startKey :: MirrorOptions -> AssociatedFile -> (Key, ActionItem) -> CommandStart
+startKey o afile (key, ai) = onlyActionOn key $ case fromToOptions o of
 	ToRemote r -> checkFailedTransferDirection ai Upload $ ifM (inAnnex key)
 		( Command.Move.toStart Command.Move.RemoveNever afile key ai =<< getParsed r
 		, do
diff --git a/Command/Move.hs b/Command/Move.hs
--- a/Command/Move.hs
+++ b/Command/Move.hs
@@ -59,8 +59,8 @@
 	case batchOption o of
 		Batch fmt -> batchFilesMatching fmt go
 		NoBatch -> withKeyOptions (keyOptions o) False
-			(startKey (fromToOptions o) (removeWhen o))
-			(withFilesInGit go)
+			(commandAction . startKey (fromToOptions o) (removeWhen o))
+			(withFilesInGit (commandAction . go))
 			=<< workTreeItems (moveFiles o)
 
 start :: FromToHereOptions -> RemoveWhen -> FilePath -> Key -> CommandStart
@@ -69,8 +69,9 @@
   where
 	afile = AssociatedFile (Just f)
 
-startKey :: FromToHereOptions -> RemoveWhen -> Key -> ActionItem -> CommandStart
-startKey fromto removewhen = start' fromto removewhen (AssociatedFile Nothing)
+startKey :: FromToHereOptions -> RemoveWhen -> (Key, ActionItem) -> CommandStart
+startKey fromto removewhen = 
+	uncurry $ start' fromto removewhen (AssociatedFile Nothing)
 
 start' :: FromToHereOptions -> RemoveWhen -> AssociatedFile -> Key -> ActionItem -> CommandStart
 start' fromto removewhen afile key ai = onlyActionOn key $
diff --git a/Command/NotifyChanges.hs b/Command/NotifyChanges.hs
--- a/Command/NotifyChanges.hs
+++ b/Command/NotifyChanges.hs
@@ -21,7 +21,7 @@
 		paramNothing (withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withNothing start
+seek = withNothing (commandAction start)
 
 start :: CommandStart
 start = go =<< watchChangedRefs
diff --git a/Command/NumCopies.hs b/Command/NumCopies.hs
--- a/Command/NumCopies.hs
+++ b/Command/NumCopies.hs
@@ -17,7 +17,7 @@
 	paramNumber (withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withWords start
+seek = withWords (commandAction . start)
 
 start :: [String] -> CommandStart
 start [] = startGet
diff --git a/Command/PreCommit.hs b/Command/PreCommit.hs
--- a/Command/PreCommit.hs
+++ b/Command/PreCommit.hs
@@ -39,7 +39,7 @@
 seek ps = lockPreCommitHook $ ifM isDirect
 	( do
 		-- update direct mode mappings for committed files
-		withWords startDirect ps
+		withWords (commandAction . startDirect) ps
 		runAnnexHook preCommitAnnexHook
 	, do
 		ifM (not <$> versionSupportsUnlockedPointers <&&> liftIO Git.haveFalseIndex)
@@ -51,14 +51,14 @@
 			, do
 				l <- workTreeItems ps
 				-- fix symlinks to files being committed
-				flip withFilesToBeCommitted l $ \f -> 
+				flip withFilesToBeCommitted l $ \f -> commandAction $
 					maybe stop (Command.Fix.start Command.Fix.FixSymlinks f)
 						=<< isAnnexLink f
 				-- inject unlocked files into the annex
 				-- (not needed when repo version uses
 				-- unlocked pointer files)
 				unlessM versionSupportsUnlockedPointers $
-					withFilesOldUnlockedToBeCommitted startInjectUnlocked l
+					withFilesOldUnlockedToBeCommitted (commandAction . startInjectUnlocked) l
 			)
 		runAnnexHook preCommitAnnexHook
 		-- committing changes to a view updates metadata
diff --git a/Command/Proxy.hs b/Command/Proxy.hs
--- a/Command/Proxy.hs
+++ b/Command/Proxy.hs
@@ -27,7 +27,7 @@
 		("-- git command") (withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withWords start
+seek = withWords (commandAction . start)
 
 start :: [String] -> CommandStart
 start [] = giveup "Did not specify command to run."
diff --git a/Command/ReKey.hs b/Command/ReKey.hs
--- a/Command/ReKey.hs
+++ b/Command/ReKey.hs
@@ -50,7 +50,7 @@
 seek :: ReKeyOptions -> CommandSeek
 seek o = case batchOption o of
 	Batch fmt -> batchInput fmt batchParser (batchCommandAction . start)
-	NoBatch -> withPairs (start . parsekey) (reKeyThese o)
+	NoBatch -> withPairs (commandAction . start . parsekey) (reKeyThese o)
   where
 	parsekey (file, skey) =
 		(file, fromMaybe (giveup "bad key") (file2key skey))
diff --git a/Command/ReadPresentKey.hs b/Command/ReadPresentKey.hs
--- a/Command/ReadPresentKey.hs
+++ b/Command/ReadPresentKey.hs
@@ -18,7 +18,7 @@
 		(withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withWords start
+seek = withWords (commandAction . start)
 
 start :: [String] -> CommandStart
 start (ks:us:[]) = do
diff --git a/Command/RecvKey.hs b/Command/RecvKey.hs
--- a/Command/RecvKey.hs
+++ b/Command/RecvKey.hs
@@ -23,7 +23,7 @@
 	paramKey (withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withKeys start
+seek = withKeys (commandAction . start)
 
 start :: Key -> CommandStart
 start key = fieldTransfer Download key $ \_p -> do
diff --git a/Command/RegisterUrl.hs b/Command/RegisterUrl.hs
--- a/Command/RegisterUrl.hs
+++ b/Command/RegisterUrl.hs
@@ -33,10 +33,10 @@
 
 seek :: RegisterUrlOptions -> CommandSeek
 seek o = case (batchOption o, keyUrlPairs o) of
-	(Batch fmt, _) -> withNothing (startMass fmt) []
+	(Batch fmt, _) -> commandAction $ startMass fmt
 	-- older way of enabling batch input, does not support BatchNull
-	(NoBatch, []) -> withNothing (startMass BatchLine) []
-	(NoBatch, ps) -> withWords start ps
+	(NoBatch, []) -> commandAction $ startMass BatchLine
+	(NoBatch, ps) -> withWords (commandAction . start) ps
 
 start :: [String] -> CommandStart
 start (keyname:url:[]) = do
@@ -69,5 +69,5 @@
 perform' :: Key -> URLString -> Annex Bool
 perform' key url = do
 	r <- Remote.claimingUrl url
-	setUrlPresent (Remote.uuid r) key (setDownloader' url r)
+	setUrlPresent key (setDownloader' url r)
 	return True
diff --git a/Command/Reinit.hs b/Command/Reinit.hs
--- a/Command/Reinit.hs
+++ b/Command/Reinit.hs
@@ -21,7 +21,7 @@
 		(withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withWords start
+seek = withWords (commandAction . start)
 
 start :: [String] -> CommandStart
 start ws = do
diff --git a/Command/Reinject.hs b/Command/Reinject.hs
--- a/Command/Reinject.hs
+++ b/Command/Reinject.hs
@@ -35,8 +35,8 @@
 
 seek :: ReinjectOptions -> CommandSeek
 seek os
-	| knownOpt os = withStrings startKnown (params os)
-	| otherwise = withWords startSrcDest (params os)
+	| knownOpt os = withStrings (commandAction . startKnown) (params os)
+	| otherwise = withWords (commandAction . startSrcDest) (params os)
 
 startSrcDest :: [FilePath] -> CommandStart
 startSrcDest (src:dest:[])
diff --git a/Command/Repair.hs b/Command/Repair.hs
--- a/Command/Repair.hs
+++ b/Command/Repair.hs
@@ -22,7 +22,7 @@
 		paramNothing (withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withNothing start
+seek = withNothing (commandAction start)
 
 start :: CommandStart
 start = next $ next $ runRepair =<< Annex.getState Annex.force
diff --git a/Command/ResolveMerge.hs b/Command/ResolveMerge.hs
--- a/Command/ResolveMerge.hs
+++ b/Command/ResolveMerge.hs
@@ -19,7 +19,7 @@
 	paramNothing (withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withNothing start
+seek = withNothing (commandAction start)
 
 start :: CommandStart
 start = do
diff --git a/Command/RmUrl.hs b/Command/RmUrl.hs
--- a/Command/RmUrl.hs
+++ b/Command/RmUrl.hs
@@ -31,7 +31,7 @@
 seek :: RmUrlOptions -> CommandSeek
 seek o = case batchOption o of
 	Batch fmt -> batchInput fmt batchParser (batchCommandAction . start)
-	NoBatch -> withPairs start (rmThese o)
+	NoBatch -> withPairs (commandAction . start) (rmThese o)
 
 -- Split on the last space, since a FilePath can contain whitespace,
 -- but a url should not.
@@ -49,5 +49,5 @@
 cleanup :: String -> Key -> CommandCleanup
 cleanup url key = do
 	r <- Remote.claimingUrl url
-	setUrlMissing (Remote.uuid r) key (setDownloader' url r)
+	setUrlMissing key (setDownloader' url r)
 	return True
diff --git a/Command/Schedule.hs b/Command/Schedule.hs
--- a/Command/Schedule.hs
+++ b/Command/Schedule.hs
@@ -20,7 +20,7 @@
 	(withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withWords start
+seek = withWords (commandAction . start)
 
 start :: [String] -> CommandStart
 start = parse
diff --git a/Command/SendKey.hs b/Command/SendKey.hs
--- a/Command/SendKey.hs
+++ b/Command/SendKey.hs
@@ -24,7 +24,7 @@
 		paramKey (withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withKeys start
+seek = withKeys (commandAction . start)
 
 start :: Key -> CommandStart
 start key = do
diff --git a/Command/SetKey.hs b/Command/SetKey.hs
--- a/Command/SetKey.hs
+++ b/Command/SetKey.hs
@@ -17,7 +17,7 @@
 	(withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withWords start
+seek = withWords (commandAction . start)
 
 start :: [String] -> CommandStart
 start (keyname:file:[]) = do
diff --git a/Command/Status.hs b/Command/Status.hs
--- a/Command/Status.hs
+++ b/Command/Status.hs
@@ -37,7 +37,7 @@
 		))
 
 seek :: StatusOptions -> CommandSeek
-seek o = withWords (start o) (statusFiles o)
+seek o = withWords (commandAction . start o) (statusFiles o)
 	
 start :: StatusOptions -> [FilePath] -> CommandStart
 start o locs = do
diff --git a/Command/Sync.hs b/Command/Sync.hs
--- a/Command/Sync.hs
+++ b/Command/Sync.hs
@@ -189,7 +189,7 @@
 			
 			whenM shouldsynccontent $ do
 				syncedcontent <- seekSyncContent o dataremotes
-				exportedcontent <- seekExportContent exportremotes
+				exportedcontent <- withbranch $ seekExportContent exportremotes
 				-- Transferring content can take a while,
 				-- and other changes can be pushed to the
 				-- git-annex branch on the remotes in the
@@ -595,7 +595,7 @@
   where
 	seekworktree mvar l bloomfeeder = seekHelper LsFiles.inRepo l >>=
 		mapM_ (\f -> ifAnnexed f (go (Right bloomfeeder) mvar (AssociatedFile (Just f))) noop)
-	seekkeys mvar bloom k _ = go (Left bloom) mvar (AssociatedFile Nothing) k
+	seekkeys mvar bloom (k, _) = go (Left bloom) mvar (AssociatedFile Nothing) k
 	go ebloom mvar af k = commandAction $ do
 		whenM (syncFile ebloom rs af k) $
 			void $ liftIO $ tryPutMVar mvar ()
@@ -680,22 +680,39 @@
  - 
  - Returns True if any file transfers were made.
  -}
-seekExportContent :: [Remote] -> Annex Bool
-seekExportContent rs = or <$> forM rs go
+seekExportContent :: [Remote] -> CurrBranch -> Annex Bool
+seekExportContent rs (currbranch, _) = or <$> forM rs go
   where
 	go r = withExclusiveLock (gitAnnexExportLock (Remote.uuid r)) $ do
 		db <- Export.openDb (Remote.uuid r)
 		ea <- Remote.exportActions r
 		exported <- case remoteAnnexExportTracking (Remote.gitconfig r) of
-			Nothing -> getExport (Remote.uuid r)
+			Nothing -> nontracking r
 			Just b -> do
 				mcur <- inRepo $ Git.Ref.tree b
 				case mcur of
-					Nothing -> getExport (Remote.uuid r)
+					Nothing -> nontracking r
 					Just cur -> do
 						Command.Export.changeExport r ea db cur
 						return [Exported cur []]
 		Export.closeDb db `after` fillexport r ea db exported
+		
+	nontracking r = do
+		exported <- getExport (Remote.uuid r)
+		maybe noop (warnnontracking r exported) currbranch
+		return exported
+	
+	warnnontracking r exported currb = inRepo (Git.Ref.tree currb) >>= \case
+		Just currt | not (any (\ex -> exportedTreeish ex == currt) exported) ->
+			showLongNote $ unwords
+				[ "Not updating export to " ++ Remote.name r
+				, "to reflect changes to the tree, because export"
+				, "tracking is not enabled. "
+				, "(Use git-annex export's --tracking option"
+				, "to enable it.)"
+				]
+		_ -> noop
+
 
 	fillexport _ _ _ [] = return False
 	fillexport r ea db (Exported { exportedTreeish = t }:[]) =
diff --git a/Command/TransferInfo.hs b/Command/TransferInfo.hs
--- a/Command/TransferInfo.hs
+++ b/Command/TransferInfo.hs
@@ -22,7 +22,7 @@
 		paramKey (withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withWords start
+seek = withWords (commandAction . start)
 
 {- Security:
  - 
diff --git a/Command/TransferKey.hs b/Command/TransferKey.hs
--- a/Command/TransferKey.hs
+++ b/Command/TransferKey.hs
@@ -42,7 +42,7 @@
 		<*> pure (fileOption v)
 
 seek :: TransferKeyOptions -> CommandSeek
-seek o = withKeys (start o) (keyOptions o)
+seek o = withKeys (commandAction . start o) (keyOptions o)
 
 start :: TransferKeyOptions -> Key -> CommandStart
 start o key = case fromToOptions o of
diff --git a/Command/TransferKeys.hs b/Command/TransferKeys.hs
--- a/Command/TransferKeys.hs
+++ b/Command/TransferKeys.hs
@@ -25,7 +25,7 @@
 	paramNothing (withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withNothing start
+seek = withNothing (commandAction start)
 
 start :: CommandStart
 start = do
diff --git a/Command/Trust.hs b/Command/Trust.hs
--- a/Command/Trust.hs
+++ b/Command/Trust.hs
@@ -23,7 +23,7 @@
 seek = trustCommand "trust" Trusted
 
 trustCommand :: String -> TrustLevel -> CmdParams -> CommandSeek
-trustCommand c level = withWords start
+trustCommand c level = withWords (commandAction . start)
   where
 	start ws = do
 		let name = unwords ws
diff --git a/Command/Unannex.hs b/Command/Unannex.hs
--- a/Command/Unannex.hs
+++ b/Command/Unannex.hs
@@ -31,7 +31,7 @@
 
 seek :: CmdParams -> CommandSeek
 seek ps = wrapUnannex $ 
-	(withFilesInGit $ whenAnnexed start) =<< workTreeItems ps
+	(withFilesInGit $ commandAction . whenAnnexed start) =<< workTreeItems ps
 
 wrapUnannex :: Annex a -> Annex a
 wrapUnannex a = ifM (versionSupportsUnlockedPointers <||> isDirect)
diff --git a/Command/Undo.hs b/Command/Undo.hs
--- a/Command/Undo.hs
+++ b/Command/Undo.hs
@@ -43,7 +43,7 @@
 	void $ Command.Sync.commitStaged Git.Branch.ManualCommit
 		"commit before undo"
 	
-	withStrings start ps
+	withStrings (commandAction . start) ps
 
 start :: FilePath -> CommandStart
 start p = do
diff --git a/Command/Ungroup.hs b/Command/Ungroup.hs
--- a/Command/Ungroup.hs
+++ b/Command/Ungroup.hs
@@ -19,7 +19,7 @@
 	(paramPair paramRemote paramDesc) (withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withWords start
+seek = withWords (commandAction . start)
 
 start :: [String] -> CommandStart
 start (name:g:[]) = do
diff --git a/Command/Uninit.hs b/Command/Uninit.hs
--- a/Command/Uninit.hs
+++ b/Command/Uninit.hs
@@ -41,9 +41,9 @@
 seek :: CmdParams -> CommandSeek
 seek ps = do
 	l <- workTreeItems ps
-	withFilesNotInGit False (whenAnnexed startCheckIncomplete) l
+	withFilesNotInGit False (commandAction . whenAnnexed startCheckIncomplete) l
 	Annex.changeState $ \s -> s { Annex.fast = True }
-	withFilesInGit (whenAnnexed Command.Unannex.start) l
+	withFilesInGit (commandAction . whenAnnexed Command.Unannex.start) l
 	finish
 
 {- git annex symlinks that are not checked into git could be left by an
diff --git a/Command/Unlock.hs b/Command/Unlock.hs
--- a/Command/Unlock.hs
+++ b/Command/Unlock.hs
@@ -30,7 +30,7 @@
 		command n SectionCommon d paramPaths (withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek ps = withFilesInGit (whenAnnexed start) =<< workTreeItems ps
+seek ps = withFilesInGit (commandAction . whenAnnexed start) =<< workTreeItems ps
 
 {- Before v6, the unlock subcommand replaces the symlink with a copy of
  - the file's content. In v6 and above, it converts the file from a symlink
diff --git a/Command/Unused.hs b/Command/Unused.hs
--- a/Command/Unused.hs
+++ b/Command/Unused.hs
@@ -303,8 +303,7 @@
 	unusedtmp <- readUnusedMap "tmp"
 	let m = unused `M.union` unusedbad `M.union` unusedtmp
 	let unusedmaps = UnusedMaps unused unusedbad unusedtmp
-	seekActions $ return $ map (a unusedmaps) $
-		concatMap (unusedSpec m) params
+	commandActions $ map (a unusedmaps) $ concatMap (unusedSpec m) params
 
 unusedSpec :: UnusedMap -> String -> [Int]
 unusedSpec m spec
diff --git a/Command/Upgrade.hs b/Command/Upgrade.hs
--- a/Command/Upgrade.hs
+++ b/Command/Upgrade.hs
@@ -19,7 +19,7 @@
 		paramNothing (withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withNothing start
+seek = withNothing (commandAction start)
 
 start :: CommandStart
 start = do
diff --git a/Command/VAdd.hs b/Command/VAdd.hs
--- a/Command/VAdd.hs
+++ b/Command/VAdd.hs
@@ -19,7 +19,7 @@
 		(withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withWords start
+seek = withWords (commandAction . start)
 
 start :: [String] -> CommandStart
 start params = do
diff --git a/Command/VCycle.hs b/Command/VCycle.hs
--- a/Command/VCycle.hs
+++ b/Command/VCycle.hs
@@ -20,7 +20,7 @@
 		paramNothing (withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withNothing start
+seek = withNothing (commandAction start)
 
 start ::CommandStart
 start = go =<< currentView
diff --git a/Command/VFilter.hs b/Command/VFilter.hs
--- a/Command/VFilter.hs
+++ b/Command/VFilter.hs
@@ -17,7 +17,7 @@
 		paramView (withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withWords start
+seek = withWords (commandAction . start)
 
 start :: [String] -> CommandStart
 start params = do
diff --git a/Command/VPop.hs b/Command/VPop.hs
--- a/Command/VPop.hs
+++ b/Command/VPop.hs
@@ -21,7 +21,7 @@
 		paramNumber (withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withWords start
+seek = withWords (commandAction . start)
 
 start :: [String] -> CommandStart
 start ps = go =<< currentView
diff --git a/Command/Vicfg.hs b/Command/Vicfg.hs
--- a/Command/Vicfg.hs
+++ b/Command/Vicfg.hs
@@ -37,7 +37,7 @@
 	paramNothing (withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withNothing start
+seek = withNothing (commandAction start)
 
 start :: CommandStart
 start = do
diff --git a/Command/View.hs b/Command/View.hs
--- a/Command/View.hs
+++ b/Command/View.hs
@@ -25,7 +25,7 @@
 		paramView (withParams seek)
 
 seek :: CmdParams -> CommandSeek
-seek = withWords start
+seek = withWords (commandAction . start)
 
 start :: [String] -> CommandStart
 start [] = giveup "Specify metadata to include in view"
diff --git a/Command/Wanted.hs b/Command/Wanted.hs
--- a/Command/Wanted.hs
+++ b/Command/Wanted.hs
@@ -30,7 +30,7 @@
   where
 	pdesc = paramPair paramRemote (paramOptional paramExpression)
 
-	seek = withWords start
+	seek = withWords (commandAction . start)
 
 	start (rname:[]) = go rname (performGet getter)
 	start (rname:expr:[]) = go rname $ \uuid -> do
diff --git a/Command/Whereis.hs b/Command/Whereis.hs
--- a/Command/Whereis.hs
+++ b/Command/Whereis.hs
@@ -43,17 +43,17 @@
 		Batch fmt -> batchFilesMatching fmt go
 		NoBatch -> 
 			withKeyOptions (keyOptions o) False
-				(startKeys m)
-				(withFilesInGit go)
+				(commandAction . startKeys m)
+				(withFilesInGit (commandAction . go))
 				=<< workTreeItems (whereisFiles o)
 
 start :: M.Map UUID Remote -> FilePath -> Key -> CommandStart
-start remotemap file key = startKeys remotemap key (mkActionItem afile)
+start remotemap file key = startKeys remotemap (key, mkActionItem afile)
   where
 	afile = AssociatedFile (Just file)
 
-startKeys :: M.Map UUID Remote -> Key -> ActionItem -> CommandStart
-startKeys remotemap key ai = do
+startKeys :: M.Map UUID Remote -> (Key, ActionItem) -> CommandStart
+startKeys remotemap (key, ai) = do
 	showStartKey "whereis" key ai
 	next $ perform remotemap key
 
diff --git a/Config.hs b/Config.hs
--- a/Config.hs
+++ b/Config.hs
@@ -102,3 +102,8 @@
 setCrippledFileSystem b = do
 	setConfig (annexConfig "crippledfilesystem") (Git.Config.boolConfig b)
 	Annex.changeGitConfig $ \c -> c { annexCrippledFileSystem = b }
+
+yesNo :: String -> Maybe Bool
+yesNo "yes" = Just True
+yesNo "no" = Just False
+yesNo _ = Nothing
diff --git a/Database/Export.hs b/Database/Export.hs
--- a/Database/Export.hs
+++ b/Database/Export.hs
@@ -126,10 +126,10 @@
 	let edirs = map
 		(\ed -> ExportedDirectory (toSFilePath (fromExportDirectory ed)) ef)
 		(exportDirectories el)
-#if MIN_VERSION_persistent(2,1,0)
-	insertMany_ edirs
+#if MIN_VERSION_persistent(2,8,1)
+	putMany edirs
 #else
-	void $ insertMany edirs
+	mapM_ insertUnique edirs
 #endif
   where
 	ik = toIKey k
@@ -208,7 +208,7 @@
 		| sha == nullSha = return Nothing
 		| otherwise = Just <$> exportKey sha
 
-updateExportTree' :: ExportHandle -> Maybe ExportKey -> Maybe ExportKey -> Git.DiffTree.DiffTreeItem-> Annex ()
+updateExportTree' :: ExportHandle -> Maybe ExportKey -> Maybe ExportKey -> Git.DiffTree.DiffTreeItem -> Annex ()
 updateExportTree' h srcek dstek i = do
 	case srcek of
 		Nothing -> return ()
diff --git a/Logs/Web.hs b/Logs/Web.hs
--- a/Logs/Web.hs
+++ b/Logs/Web.hs
@@ -56,20 +56,32 @@
 	. map (fst . getDownloader)
 	<$> getUrls key
 
-setUrlPresent :: UUID -> Key -> URLString -> Annex ()
-setUrlPresent uuid key url = do
+setUrlPresent :: Key -> URLString -> Annex ()
+setUrlPresent key url = do
 	us <- getUrls key
 	unless (url `elem` us) $ do
 		config <- Annex.getGitConfig
 		addLog (urlLogFile config key) =<< logNow InfoPresent url
-	logChange key uuid InfoPresent
+	-- If the url does not have an OtherDownloader, it must be present
+	-- in the web.
+	case snd (getDownloader url) of
+		OtherDownloader -> return ()
+		_ -> logChange key webUUID InfoPresent
 
-setUrlMissing :: UUID -> Key -> URLString -> Annex ()
-setUrlMissing uuid key url = do
+setUrlMissing :: Key -> URLString -> Annex ()
+setUrlMissing key url = do
 	config <- Annex.getGitConfig
 	addLog (urlLogFile config key) =<< logNow InfoMissing url
-	whenM (null <$> getUrls key) $
-		logChange key uuid InfoMissing
+	-- If the url was a web url (not OtherDownloader) and none of
+	-- the remaining urls for the key are web urls, the key must not
+	-- be present in the web.
+	when (isweb url) $
+		whenM (null . filter isweb <$> getUrls key) $
+			logChange key webUUID InfoMissing
+  where
+	isweb u = case snd (getDownloader u) of
+		OtherDownloader -> False
+		_ -> True
 
 {- Finds all known urls. -}
 knownUrls :: Annex [(Key, URLString)]
diff --git a/Remote/BitTorrent.hs b/Remote/BitTorrent.hs
--- a/Remote/BitTorrent.hs
+++ b/Remote/BitTorrent.hs
@@ -112,7 +112,7 @@
 
 dropKey :: Key -> Annex Bool
 dropKey k = do
-	mapM_ (setUrlMissing bitTorrentUUID k) =<< getBitTorrentUrls k
+	mapM_ (setUrlMissing k) =<< getBitTorrentUrls k
 	return True
 
 {- We punt and don't try to check if a torrent has enough seeders
diff --git a/Remote/External.hs b/Remote/External.hs
--- a/Remote/External.hs
+++ b/Remote/External.hs
@@ -416,9 +416,9 @@
 			<$> getRemoteState (externalUUID external) key
 		send $ VALUE state
 	handleRemoteRequest (SETURLPRESENT key url) =
-		setUrlPresent (externalUUID external) key url
+		setUrlPresent key url
 	handleRemoteRequest (SETURLMISSING key url) =
-		setUrlMissing (externalUUID external) key url
+		setUrlMissing key url
 	handleRemoteRequest (SETURIPRESENT key uri) =
 		withurl (SETURLPRESENT key) uri
 	handleRemoteRequest (SETURIMISSING key uri) =
diff --git a/Remote/Git.hs b/Remote/Git.hs
--- a/Remote/Git.hs
+++ b/Remote/Git.hs
@@ -254,7 +254,7 @@
 		v <- liftIO $ withTmpFile "git-annex.tmp" $ \tmpfile h -> do
 			hClose h
 			let url = Git.repoLocation r ++ "/config"
-			ifM (Url.download nullMeterUpdate url tmpfile uo)
+			ifM (Url.downloadQuiet nullMeterUpdate url tmpfile uo)
 				( Just <$> pipedconfig "git" [Param "config", Param "--null", Param "--list", Param "--file", File tmpfile]
 				, return Nothing
 				)
diff --git a/Remote/Helper/Encryptable.hs b/Remote/Helper/Encryptable.hs
--- a/Remote/Helper/Encryptable.hs
+++ b/Remote/Helper/Encryptable.hs
@@ -25,6 +25,7 @@
 
 import Annex.Common
 import Types.Remote
+import Config
 import Crypto
 import Types.Crypto
 import qualified Annex
@@ -128,11 +129,9 @@
  - Not when a shared cipher is used.
  -}
 embedCreds :: RemoteConfig -> Bool
-embedCreds c
-	| M.lookup "embedcreds" c == Just "yes" = True
-	| M.lookup "embedcreds" c == Just "no" = False
-	| isJust (M.lookup "cipherkeys" c) && isJust (M.lookup "cipher" c) = True
-	| otherwise = False
+embedCreds c = case yesNo =<< M.lookup "embedcreds" c of
+	Just v -> v
+	Nothing -> isJust (M.lookup "cipherkeys" c) && isJust (M.lookup "cipher" c)
 
 {- Gets encryption Cipher, and key encryptor. -}
 cipherKey :: RemoteConfig -> RemoteGitConfig -> Annex (Maybe (Cipher, EncKey))
diff --git a/Remote/Helper/Export.hs b/Remote/Helper/Export.hs
--- a/Remote/Helper/Export.hs
+++ b/Remote/Helper/Export.hs
@@ -17,6 +17,7 @@
 import Remote.Helper.Encryptable (isEncrypted)
 import Database.Export
 import Annex.Export
+import Config
 
 import qualified Data.Map as M
 import Control.Concurrent.STM
@@ -69,13 +70,16 @@
 -- remote to be an export.
 adjustExportable :: Remote -> Annex Remote
 adjustExportable r = case M.lookup "exporttree" (config r) of
-	Just "yes" -> ifM (isExportSupported r)
-		( isexport
-		, notexport
-		)
 	Nothing -> notexport
-	Just "no" -> notexport
-	Just _ -> error "bad exporttree value"
+	Just c -> case yesNo c of
+		Just True -> ifM (isExportSupported r)
+			( isexport
+			, notexport
+			)
+		Just False -> notexport
+		Nothing -> do
+			warning $ "bad exporttree value for " ++ name r ++ ", assuming not an export"
+			notexport
   where
 	notexport = return $ r 
 		{ exportActions = exportUnsupported
diff --git a/Remote/Rsync.hs b/Remote/Rsync.hs
--- a/Remote/Rsync.hs
+++ b/Remote/Rsync.hs
@@ -114,7 +114,7 @@
 	, rsyncOptions = transport ++ opts []
 	, rsyncUploadOptions = transport ++ opts (remoteAnnexRsyncUploadOptions gc)
 	, rsyncDownloadOptions = transport ++ opts (remoteAnnexRsyncDownloadOptions gc)
-	, rsyncShellEscape = M.lookup "shellescape" c /= Just "no"
+	, rsyncShellEscape = (yesNo =<< M.lookup "shellescape" c) /= Just False
 	}
   where
 	opts specificopts = map Param $ filter safe $
diff --git a/Remote/S3.hs b/Remote/S3.hs
--- a/Remote/S3.hs
+++ b/Remote/S3.hs
@@ -195,7 +195,7 @@
 	void $ storeHelper info h f (T.pack $ bucketObject info k) p
 	-- Store public URL to item in Internet Archive.
 	when (isIA info && not (isChunkKey k)) $
-		setUrlPresent webUUID k (iaPublicUrl info (bucketObject info k))
+		setUrlPresent k (iaPublicUrl info (bucketObject info k))
 	return True
 
 storeHelper :: S3Info -> S3Handle -> FilePath -> S3.Object -> MeterUpdate -> Annex (Maybe S3VersionID)
@@ -605,9 +605,7 @@
 		, host = M.lookup "host" c
 		}
   where
-	boolcfg k = case M.lookup k c of
-		Just "yes" -> True
-		_ -> False
+	boolcfg k = fromMaybe False $ yesNo =<< M.lookup k c
 
 putObject :: S3Info -> T.Text -> RequestBody -> S3.PutObject
 putObject info file rbody = (S3.putObject (bucket info) file rbody)
diff --git a/Remote/Tahoe.hs b/Remote/Tahoe.hs
--- a/Remote/Tahoe.hs
+++ b/Remote/Tahoe.hs
@@ -104,7 +104,7 @@
 	u <- maybe (liftIO genUUID) return mu
 	configdir <- liftIO $ defaultTahoeConfigDir u
 	scs <- liftIO $ tahoeConfigure configdir furl (M.lookup scsk c)
-	let c' = if M.lookup "embedcreds" c == Just "yes"
+	let c' = if (yesNo =<< M.lookup "embedcreds" c) == Just True
 		then flip M.union c $ M.fromList
 			[ (furlk, furl)
 			, (scsk, scs)
diff --git a/Remote/Web.hs b/Remote/Web.hs
--- a/Remote/Web.hs
+++ b/Remote/Web.hs
@@ -100,7 +100,7 @@
 
 dropKey :: Key -> Annex Bool
 dropKey k = do
-	mapM_ (setUrlMissing webUUID k) =<< getWebUrls k
+	mapM_ (setUrlMissing k) =<< getWebUrls k
 	return True
 
 checkKey :: Key -> Annex Bool
diff --git a/Types/GitConfig.hs b/Types/GitConfig.hs
--- a/Types/GitConfig.hs
+++ b/Types/GitConfig.hs
@@ -26,6 +26,7 @@
 import Types.UUID
 import Types.Distribution
 import Types.Availability
+import Types.Concurrency
 import Types.NumCopies
 import Types.Difference
 import Types.RefSpec
@@ -99,6 +100,7 @@
 	, annexAllowedHttpAddresses :: String
 	, annexAllowUnverifiedDownloads :: Bool
 	, annexMaxExtensionLength :: Maybe Int
+	, annexJobs :: Concurrency
 	, coreSymlinks :: Bool
 	, coreSharedRepository :: SharedRepository
 	, receiveDenyCurrentBranch :: DenyCurrentBranch
@@ -173,6 +175,7 @@
 	, annexAllowUnverifiedDownloads = (== Just "ACKTHPPT") $
 		getmaybe (annex "security.allow-unverified-downloads")
 	, annexMaxExtensionLength = getmayberead (annex "maxextensionlength")
+	, annexJobs = maybe NonConcurrent Concurrent $ getmayberead (annex "jobs")
 	, coreSymlinks = getbool "core.symlinks" True
 	, coreSharedRepository = getSharedRepository r
 	, receiveDenyCurrentBranch = getDenyCurrentBranch r
diff --git a/Utility/Url.hs b/Utility/Url.hs
--- a/Utility/Url.hs
+++ b/Utility/Url.hs
@@ -29,6 +29,7 @@
 	getUrlInfo,
 	assumeUrlExists,
 	download,
+	downloadQuiet,
 	sinkResponseFile,
 	downloadPartial,
 	parseURIRelaxed,
@@ -139,19 +140,21 @@
 		]
 	schemelist = map fromScheme $ S.toList $ allowedSchemes uo
 
-checkPolicy :: UrlOptions -> URI -> a -> IO a -> IO a
-checkPolicy uo u onerr a
+checkPolicy :: UrlOptions -> URI -> a -> (String -> IO b) -> IO a -> IO a
+checkPolicy uo u onerr displayerror a
 	| allowedScheme uo u = a
 	| otherwise = do
-		hPutStrLn stderr $ 
+		void $ displayerror $
 			"Configuration does not allow accessing " ++ show u
-		hFlush stderr
 		return onerr
 
-unsupportedUrlScheme :: URI -> IO ()
-unsupportedUrlScheme u = do
-	hPutStrLn stderr $ 
-		"Unsupported url scheme " ++ show u
+unsupportedUrlScheme :: URI -> (String -> IO a) -> IO a
+unsupportedUrlScheme u displayerror =
+	displayerror $ "Unsupported url scheme " ++ show u
+
+warnError :: String -> IO ()
+warnError msg = do
+	hPutStrLn stderr msg
 	hFlush stderr
 
 allowedScheme :: UrlOptions -> URI -> Bool
@@ -192,15 +195,15 @@
  - also returning its size and suggested filename if available. -}
 getUrlInfo :: URLString -> UrlOptions -> IO UrlInfo
 getUrlInfo url uo = case parseURIRelaxed url of
-	Just u -> checkPolicy uo u dne $
-		case (urlDownloader uo, parseUrlConduit (show u)) of
+	Just u -> checkPolicy uo u dne warnError $
+		case (urlDownloader uo, parseUrlRequest (show u)) of
 			(DownloadWithConduit, Just req) ->
 				existsconduit req
 					`catchNonAsync` (const $ return dne)
 			(DownloadWithConduit, Nothing)
 				| isfileurl u -> existsfile u
 				| otherwise -> do
-					unsupportedUrlScheme u
+					unsupportedUrlScheme u warnError
 					return dne
 			(DownloadWithCurl _, _) 
 				| isfileurl u -> existsfile u
@@ -294,42 +297,46 @@
  - Displays error message on stderr when download failed.
  -}
 download :: MeterUpdate -> URLString -> FilePath -> UrlOptions -> IO Bool
-download meterupdate url file uo =
+download = download' False
+
+{- Avoids displaying any error message. -}
+downloadQuiet :: MeterUpdate -> URLString -> FilePath -> UrlOptions -> IO Bool
+downloadQuiet = download' True
+
+download' :: Bool -> MeterUpdate -> URLString -> FilePath -> UrlOptions -> IO Bool
+download' noerror meterupdate url file uo =
 	catchJust matchHttpException go showhttpexception
-		`catchNonAsync` showerr
+		`catchNonAsync` (dlfailed . show)
   where
 	go = case parseURIRelaxed url of
-		Just u -> checkPolicy uo u False $
-			case (urlDownloader uo, parseUrlConduit (show u)) of
+		Just u -> checkPolicy uo u False dlfailed $
+			case (urlDownloader uo, parseUrlRequest (show u)) of
 				(DownloadWithConduit, Just req) ->
 					downloadconduit req
 				(DownloadWithConduit, Nothing)
 					| isfileurl u -> downloadfile u
-					| otherwise -> do
-						unsupportedUrlScheme u
-						return False
+					| otherwise -> unsupportedUrlScheme u dlfailed
 				(DownloadWithCurl _, _)
 					| isfileurl u -> downloadfile u
 					| otherwise -> downloadcurl
 		Nothing -> do
 			liftIO $ debugM "url" url
-			hPutStrLn stderr "download failed: invalid url"
-			return False
+			dlfailed "invalid url"
 	
 	isfileurl u = uriScheme u == "file:"
 
 	downloadconduit req = catchMaybeIO (getFileSize file) >>= \case
 		Nothing -> runResourceT $ do
 			liftIO $ debugM "url" (show req')
-			resp <- http (applyRequest uo req') (httpManager uo)
+			resp <- http req' (httpManager uo)
 			if responseStatus resp == ok200
 				then store zeroBytesProcessed WriteMode resp
 				else showrespfailure resp
 		Just sz -> resumeconduit req' sz
 	  where
-		-- Override http-client's default decompression of gzip
-		-- compressed files. We want the unmodified file content.
-		req' = req
+		req' = applyRequest uo $ req
+			-- Override http-client's default decompression of gzip
+			-- compressed files. We want the unmodified file content.
 			{ requestHeaders = (hAcceptEncoding, "identity") :
 				filter ((/= hAcceptEncoding) . fst)
 					(requestHeaders req)
@@ -361,11 +368,8 @@
 					then store zeroBytesProcessed WriteMode resp
 					else showrespfailure resp
 	
-	showrespfailure resp = liftIO $ do
-		hPutStrLn stderr $ B8.toString $
-			statusMessage $ responseStatus resp
-		hFlush stderr
-		return False
+	showrespfailure = liftIO . dlfailed . B8.toString 
+		. statusMessage . responseStatus
 	showhttpexception he = do
 #if MIN_VERSION_http_client(0,5,0)
 		let msg = case he of
@@ -383,13 +387,13 @@
 				B8.toString (statusMessage status)
 			_ -> show he
 #endif
-		hPutStrLn stderr $ "download failed: " ++ msg
-		hFlush stderr
-		return False
-	showerr e = do
-		hPutStrLn stderr (show e)
-		hFlush stderr
-		return False
+		dlfailed msg
+	dlfailed msg
+		| noerror = return False
+		| otherwise = do
+			hPutStrLn stderr $ "download failed: " ++ msg
+			hFlush stderr
+			return False
 	
 	store initialp mode resp = do
 		sinkResponseFile meterupdate initialp file mode resp
@@ -401,13 +405,15 @@
 		unlessM (doesFileExist file) $
 			writeFile file ""
 		let ps = curlParams uo
-			[ Param "-sS"
+			[ if noerror
+				then Param "-S"
+				else Param "-sS"
 			, Param "-f"
 			, Param "-L"
 			, Param "-C", Param "-"
 			]
 		boolSystem "curl" (ps ++ [Param "-o", File file, File url])
-					
+	
 	downloadfile u = do
 		let src = unEscapeString (uriPath u)
 		withMeteredFile src meterupdate $
@@ -455,36 +461,23 @@
 	Nothing -> return Nothing
 	Just u -> go u `catchNonAsync` const (return Nothing)
   where
-	go u = case parseUrlConduit (show u) of
+	go u = case parseUrlRequest (show u) of
 		Nothing -> return Nothing
 		Just req -> do
 			let req' = applyRequest uo req
 			liftIO $ debugM "url" (show req')
 			withResponse req' (httpManager uo) $ \resp ->
 				if responseStatus resp == ok200
-					then Just <$> brread n [] (responseBody resp)
+					then Just <$> brReadSome (responseBody resp) n
 					else return Nothing
 
-	-- could use brReadSome here, needs newer http-client dependency
-	brread n' l rb
-		| n' <= 0 = return (L.fromChunks (reverse l))
-		| otherwise = do
-			bs <- brRead rb
-			if B.null bs
-				then return (L.fromChunks (reverse l))
-				else brread (n' - B.length bs) (bs:l) rb
-
 {- Allows for spaces and other stuff in urls, properly escaping them. -}
 parseURIRelaxed :: URLString -> Maybe URI
 parseURIRelaxed s = maybe (parseURIRelaxed' s) Just $
 	parseURI $ escapeURIString isAllowedInURI s
 
-parseUrlConduit :: URLString -> Maybe Request
-#if MIN_VERSION_http_client(0,4,30)
-parseUrlConduit = parseUrlThrow
-#else
-parseUrlConduit = parseUrl
-#endif
+parseUrlRequest :: URLString -> Maybe Request
+parseUrlRequest = parseUrlThrow
 
 {- Some characters like '[' are allowed in eg, the address of
  - an uri, but cannot appear unescaped further along in the uri.
diff --git a/doc/git-annex-export.mdwn b/doc/git-annex-export.mdwn
--- a/doc/git-annex-export.mdwn
+++ b/doc/git-annex-export.mdwn
@@ -43,7 +43,7 @@
 that, it can be used as a key/value store and the limitations in the above
 paragraph do not appy. Note that dropping content from such a remote is
 not supported. See individual special remotes' documentation for
-details of how to  enable such versioning.
+details of how to enable such versioning.
 
 # OPTIONS
 
@@ -55,7 +55,11 @@
 
   This makes the export track changes that are committed to
   the branch. `git annex sync --content` and the git-annex assistant
-  will update exports when it commits to the branch they are tracking.
+  will update exports with commits made to the branch.
+
+  This is a local configuration setting, similar to a git remote's tracking
+  branch. You'll need to run `git annex export --tracking` in each
+  repository you want the export to track.
 
 * `--fast`
 
diff --git a/doc/git-annex-metadata.mdwn b/doc/git-annex-metadata.mdwn
--- a/doc/git-annex-metadata.mdwn
+++ b/doc/git-annex-metadata.mdwn
@@ -23,11 +23,17 @@
 automatically.
 
 Note that the metadata is attached to the particular content of a file,
-not to a particular filename on a particular git branch. 
-All files with the same content share the same metadata, which is
+not to a particular filename on a particular git branch.  More precisely,
+metadata is attached to the key used for the file, which can reflect
+file contents and/or name, depending on the key-value backend used for the file.
+All files with the same key share the same metadata, which is
 stored in the git-annex branch. If a file is edited, old
-metadata will be copied to the new contents when you [[git-annex-add]]
-the edited file. 
+metadata will be copied to the new key when you [[git-annex-add]]
+the edited file.  Note also that changes to a file's git-annex metadata will
+not be reflected in the git log of the file, since they are stored on the 
+git-annex branch. To attach metadata to a particular path,
+rather than a particular key, use .gitattributes .
+
 
 # OPTIONS
 
diff --git a/doc/git-annex-rmurl.mdwn b/doc/git-annex-rmurl.mdwn
--- a/doc/git-annex-rmurl.mdwn
+++ b/doc/git-annex-rmurl.mdwn
@@ -10,6 +10,9 @@
 
 Record that the file is no longer available at the url.
 
+Removing the last url will make git-annex no longer treat content as being
+present in the web special remote.
+
 # OPTIONS
 
 * `--batch`
diff --git a/doc/git-annex.mdwn b/doc/git-annex.mdwn
--- a/doc/git-annex.mdwn
+++ b/doc/git-annex.mdwn
@@ -935,6 +935,13 @@
   This controls which refs `git-annex unused` considers to be used.
   See REFSPEC FORMAT in [[git-annex-unused]](1) for details.
 
+* `annex.jobs
+
+  Configure the number of concurrent jobs to run. Default is 1.
+
+  Only git-annex commands that support the --jobs option will
+  use this.
+
 * `annex.queuesize`
 
   git-annex builds a queue of git commands, in order to combine similar
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: 6.20180926
+Version: 6.20181011
 Cabal-Version: >= 1.8
 License: GPL-3
 Maintainer: Joey Hess <id@joeyh.name>
