Recent changes to this wiki:

annex.maxextensions configuration
Controls how many filename extensions to preserve.
Sponsored-by: the NIH-funded NICEMAN (ReproNim TR&D3) project
diff --git a/Annex/View.hs b/Annex/View.hs
index 7372287380..482ce17c3a 100644
--- a/Annex/View.hs
+++ b/Annex/View.hs
@@ -387,7 +387,7 @@ prop_view_roundtrips (AssociatedFile Nothing) _ _ = True
 prop_view_roundtrips (AssociatedFile (Just f)) metadata visible = or
 	[ B.null (P.takeFileName f) && B.null (P.takeDirectory f)
 	, viewTooLarge view
-	, all hasfields (viewedFiles view (viewedFileFromReference' Nothing) (fromRawFilePath f) metadata)
+	, all hasfields (viewedFiles view (viewedFileFromReference' Nothing Nothing) (fromRawFilePath f) metadata)
 	]
   where
 	view = View (Git.Ref "foo") $
diff --git a/Annex/View/ViewedFile.hs b/Annex/View/ViewedFile.hs
index 6aa992babb..84dcbc897a 100644
--- a/Annex/View/ViewedFile.hs
+++ b/Annex/View/ViewedFile.hs
@@ -1,6 +1,6 @@
 {- filenames (not paths) used in views
  -
- - Copyright 2014-2023 Joey Hess <id@joeyh.name>
+ - Copyright 2014-2024 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -19,6 +19,7 @@ module Annex.View.ViewedFile (
 
 import Annex.Common
 import Utility.QuickCheck
+import Backend.Utilities (maxExtensions)
 
 import qualified Data.ByteString as S
 
@@ -37,10 +38,12 @@ type MkViewedFile = FilePath -> ViewedFile
  - So, from dir/subdir/file.foo, generate file_%dir%subdir%.foo
  -}
 viewedFileFromReference :: GitConfig -> MkViewedFile
-viewedFileFromReference g = viewedFileFromReference' (annexMaxExtensionLength g)
+viewedFileFromReference g = viewedFileFromReference'
+	(annexMaxExtensionLength g)
+	(annexMaxExtensions g)
 
-viewedFileFromReference' :: Maybe Int -> MkViewedFile
-viewedFileFromReference' maxextlen f = concat $
+viewedFileFromReference' :: Maybe Int -> Maybe Int -> MkViewedFile
+viewedFileFromReference' maxextlen maxextensions f = concat $
 	[ escape (fromRawFilePath base')
 	, if null dirs then "" else "_%" ++ intercalate "%" (map escape dirs) ++ "%"
 	, escape $ fromRawFilePath $ S.concat extensions'
@@ -51,11 +54,12 @@ viewedFileFromReference' maxextlen f = concat $
 	(base, extensions) = case maxextlen of
 		Nothing -> splitShortExtensions (toRawFilePath basefile')
 		Just n -> splitShortExtensions' (n+1) (toRawFilePath basefile')
-	{- Limit to two extensions maximum. -}
+	{- Limit number of extensions. -}
+	maxextensions' = fromMaybe maxExtensions maxextensions
 	(base', extensions')
-		| length extensions <= 2 = (base, extensions)
+		| length extensions <= maxextensions' = (base, extensions)
 		| otherwise = 
-			let (es,more) = splitAt 2 (reverse extensions)
+			let (es,more) = splitAt maxextensions' (reverse extensions)
 			in (base <> mconcat (reverse more), reverse es)
 	{- On Windows, if the filename looked like "dir/c:foo" then
 	 - basefile would look like it contains a drive letter, which will
@@ -101,7 +105,8 @@ prop_viewedFile_roundtrips tf
 	-- Relative filenames wanted, not directories.
 	| any (isPathSeparator) (end f ++ beginning f) = True
 	| isAbsolute f || isDrive f = True
-	| otherwise = dir == dirFromViewedFile (viewedFileFromReference' Nothing f)
+	| otherwise = dir == dirFromViewedFile 
+		(viewedFileFromReference' Nothing Nothing f)
   where
 	f = fromTestableFilePath tf
 	dir = joinPath $ beginning $ splitDirectories f
diff --git a/Backend/Hash.hs b/Backend/Hash.hs
index 9768550adf..fc0a8b8591 100644
--- a/Backend/Hash.hs
+++ b/Backend/Hash.hs
@@ -170,11 +170,14 @@ needsUpgrade key = or
 	]
 
 trivialMigrate :: Key -> Backend -> AssociatedFile -> Bool -> Annex (Maybe Key)
-trivialMigrate oldkey newbackend afile _inannex = trivialMigrate' oldkey newbackend afile
-	<$> (annexMaxExtensionLength <$> Annex.getGitConfig)
-
-trivialMigrate' :: Key -> Backend -> AssociatedFile -> Maybe Int -> Maybe Key
-trivialMigrate' oldkey newbackend afile maxextlen
+trivialMigrate oldkey newbackend afile _inannex = do
+	c <- Annex.getGitConfig
+	return $ trivialMigrate' oldkey newbackend afile
+		(annexMaxExtensionLength c)
+		(annexMaxExtensions c)
+
+trivialMigrate' :: Key -> Backend -> AssociatedFile -> Maybe Int -> Maybe Int -> Maybe Key
+trivialMigrate' oldkey newbackend afile maxextlen maxexts
 	{- Fast migration from hashE to hash backend. -}
 	| migratable && hasExt oldvariety = Just $ alterKey oldkey $ \d -> d
 		{ keyName = S.toShort (keyHash oldkey)
@@ -185,7 +188,7 @@ trivialMigrate' oldkey newbackend afile maxextlen
 		AssociatedFile Nothing -> Nothing
 		AssociatedFile (Just file) -> Just $ alterKey oldkey $ \d -> d
 			{ keyName = S.toShort $ keyHash oldkey 
-				<> selectExtension maxextlen file
+				<> selectExtension maxextlen maxexts file
 			, keyVariety = newvariety
 			}
 	{- Upgrade to fix bad previous migration that created a
diff --git a/Backend/Utilities.hs b/Backend/Utilities.hs
index 3b68eed624..304cfaac16 100644
--- a/Backend/Utilities.hs
+++ b/Backend/Utilities.hs
@@ -45,20 +45,24 @@ genKeyName s
  - file that the key was generated from.  -}
 addE :: KeySource -> (KeyVariety -> KeyVariety) -> Key -> Annex Key
 addE source sethasext k = do
-	maxlen <- annexMaxExtensionLength <$> Annex.getGitConfig
-	let ext = selectExtension maxlen (keyFilename source)
+	c <- Annex.getGitConfig
+	let ext = selectExtension
+		(annexMaxExtensionLength c)
+		(annexMaxExtensions c)
+		(keyFilename source)
 	return $ alterKey k $ \d -> d
 		{ keyName = keyName d <> S.toShort ext
 		, keyVariety = sethasext (keyVariety d)
 		}
 
-selectExtension :: Maybe Int -> RawFilePath -> S.ByteString
-selectExtension maxlen f
+selectExtension :: Maybe Int -> Maybe Int -> RawFilePath -> S.ByteString
+selectExtension maxlen maxextensions f
 	| null es = ""
 	| otherwise = S.intercalate "." ("":es)
   where
 	es = filter (not . S.null) $ reverse $
-		take 2 $ filter (S.all validInExtension) $
+		take (fromMaybe maxExtensions maxextensions) $
+		filter (S.all validInExtension) $
 		takeWhile shortenough $
 		reverse $ S.split (fromIntegral (ord '.')) (P.takeExtensions f')
 	shortenough e = S.length e <= fromMaybe maxExtensionLen maxlen
@@ -75,3 +79,6 @@ validInExtension c
 
 maxExtensionLen :: Int
 maxExtensionLen = 4 -- long enough for "jpeg"
+
+maxExtensions :: Int
+maxExtensions = 2 -- include both extensions of "tar.gz"
diff --git a/CHANGELOG b/CHANGELOG
index 5e73fd0dac..735d81282c 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -18,6 +18,8 @@ git-annex (10.20240228) UNRELEASED; urgency=medium
   * Added rclone special remote, which can be used without needing
     to install the git-annex-remote-rclone program. This needs
     a new version of rclone, which supports "rclone gitannex".
+  * annex.maxextensions configuration controls how many filename
+    extensions to preserve.
 
  -- Joey Hess <id@joeyh.name>  Tue, 27 Feb 2024 13:07:10 -0400
 
diff --git a/Types/GitConfig.hs b/Types/GitConfig.hs
index f97ee52192..26540b8484 100644
--- a/Types/GitConfig.hs
+++ b/Types/GitConfig.hs
@@ -136,6 +136,7 @@ data GitConfig = GitConfig
 	, annexAllowedIPAddresses :: String
 	, annexAllowUnverifiedDownloads :: Bool
 	, annexMaxExtensionLength :: Maybe Int
+	, annexMaxExtensions :: Maybe Int
 	, annexJobs :: Concurrency
 	, annexCacheCreds :: Bool
 	, annexAutoUpgradeRepository :: Bool
@@ -244,6 +245,7 @@ extractGitConfig configsource r = GitConfig
 	, annexAllowUnverifiedDownloads = (== Just "ACKTHPPT") $
 		getmaybe (annexConfig "security.allow-unverified-downloads")
 	, annexMaxExtensionLength = getmayberead (annexConfig "maxextensionlength")
+	, annexMaxExtensions = getmayberead (annexConfig "maxextensions")
 	, annexJobs = fromMaybe NonConcurrent $ 
 		parseConcurrency =<< getmaybe (annexConfig "jobs")
 	, annexCacheCreds = getbool (annexConfig "cachecreds") True
diff --git a/doc/git-annex.mdwn b/doc/git-annex.mdwn
index 60acd0573c..59dacb0229 100644
--- a/doc/git-annex.mdwn
+++ b/doc/git-annex.mdwn
@@ -873,8 +873,16 @@ repository, using [[git-annex-config]]. See its man page for a list.)
   and also when generating a view branch.
 
   The default length is 4, which allows extensions like "jpeg". The dot before
-  the extension is not counted part of its length. At most two extensions
-  at the end of a filename will be preserved, e.g. .gz or .tar.gz .
+  the extension is not counted part of its length.
+
+* `annex.maxextensions`
+
+  Maximum number of filename extensions to preserve when using a backend
+  that preserves filename extensions, and also when generating a view
+  branch.
+

(Diff truncated)
comment
diff --git a/doc/todo/way_to_instruct_on_how_to_decide_on_extension__63__/comment_1_b3e9fcb09b6455301a5f7a9bf50a8a49._comment b/doc/todo/way_to_instruct_on_how_to_decide_on_extension__63__/comment_1_b3e9fcb09b6455301a5f7a9bf50a8a49._comment
new file mode 100644
index 0000000000..55d8038989
--- /dev/null
+++ b/doc/todo/way_to_instruct_on_how_to_decide_on_extension__63__/comment_1_b3e9fcb09b6455301a5f7a9bf50a8a49._comment
@@ -0,0 +1,22 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 1"""
+ date="2024-04-18T17:39:52Z"
+ content="""
+Of course, choosing a backend that does not include extension is something
+worth considering. Unless something needs the object file to preserve the
+extension. For a .mkv file, I'd guess most video players don't care about
+the extension.
+
+annex.maxextensionlength won't help here, but I think it makes sense to add
+an analagous annex.maxextensioncount which would default to 2 (as it
+currently does to handle .tar.gz) but you could set to 1.
+
+It might also be a reasonable argument that filename extensions are not
+just numbers, but then again, foo.1.pdf foo.2.pdf is a pretty common kind
+of pattern, although the extent that the numbers in that are extensions
+with any meaning to a program would depend. Some archivers do eg split
+files into foo.1 foo.2 foo.3 and use the extensions to get them back.
+Anyway, the same kind of problem could happen when not using only
+numbers.
+"""]]

update to focus on why this is still open
diff --git a/doc/todo/transitive_transfers.mdwn b/doc/todo/transitive_transfers.mdwn
index 9710402f04..ff0e57ba9f 100644
--- a/doc/todo/transitive_transfers.mdwn
+++ b/doc/todo/transitive_transfers.mdwn
@@ -1,77 +1,29 @@
-I have this situation:
+[[!meta title="streaming for transitive transfers"]]
 
-* `marcos`: home server, canonical repository with all my files
-  (`group=backup`)
-* `angela`: laptop, with a subset of the files (`group=manual`)
-* `VHS`: backup external USB storage, should have a redundant copy of
-  all files (`group=manual`)
+This todo was originally about `git-annex copy --from --to`, which got
+implemented, but without the ability to stream content between the two
+remotes.
 
-directly connecting the external USB drive to `marcos` is annoying, so
-I usually connect it to `angela` instead, which doesn't have all the
-files.
+So this todo remains open, but is now only concerned with
+streaming an object that is being received from one remote out to another
+remote without first needing to buffer the whole object on disk.
 
-This brings up the peculiar situation that I cannot actually backup
-all the files to `VHS` from `angela`, without first copying them
-locally.
+git-annex's remote interface does not currently support that.
+`retrieveKeyFile` stores the object into a file. And `storeKey`
+sends an object file to a remote.
 
-I have a few issues with that:
+The interface would need to be extended to support this, and the external
+special remote interface as well. As well as git remotes and special
+remotes needing to be updated to support the new interface.
 
- 1. it fails silently: if I try to copy to `VHS` and the file is not on
-    `angela`, it silently fails:
-   
-        [997]anarcat@angela:mp3$ git annex drop nothere
-        (recording state in git...)
-        [998]anarcat@angela:mp3$ git annex copy --to VHS nothere
-        [999]anarcat@angela:mp3$ git annex find --in VHS nothere
-        [1001]anarcat@angela:mp3$ git annex list nothere
-        here
-        |VHS
-        ||htcones
-        |||marcos
-        ||||web
-        |||||bittorrent
-        ||||||htconesdumb
-        |||||||
-        ___X___ nothere
-        [1002]anarcat@angela:mp3$
-        
-    this shouldn't silently fail to copy: it should warn me that it
-    can't find a file to copy, at least.
+There's also a question of buffering. If a object is being received from a
+remote faster than it is being sent to the other remote, it has to be
+buffered somewhere, eg not in memory. Or the receive interface needs to
+include a way to get the sender to pause.
 
- 1. it takes up more disk space: i need to download all the missing
-    files locally before I can transfer them to `VHS`. here's the way
-    I make sure files are transfered properly on `VHS`:
-   
-        git annex copy --to VHS --not --in VHS
-        git annex get --not --in VHS
-        git annex copy --to VHS --not --in VHS
-        git annex drop --not --in 'here@{yesterday}'
-        
-    the latter line is expecially problematic, because it is not
-    accurate...
+----
 
- 1. it's slower: i need to write files locally before I can transfer
-    them. ideally, those files would be streamed, or at least I would
-    need to buffer locally only one file at a time and not the whole
-    batch.
-
-Maybe I am missing something obvious here and there are other ways of
-doing this. I am running `6.20160902+gitgbc49d8a-1~ndall+1`.
-
-I know I could setup `angela` to be in the `transfer` group, but then
-files I don't want would end up stored on `angela`: files that are
-missing from other remotes, for example. Even worse, some files I *do*
-want could be candidates for removal on `angela` because they have
-been propagated everywhere, whereas I have a select set of files
-(hence `group=manual`) that are present in `angela` that I want to
-stay there.
-
-It seems to me at least #1 above should be fixed: `copy` shouldn't
-succeed when it can't comply with the requested preferred content
-expression.
-
-Somehow, I expected this to work, and maybe that's the core issue here:
-
-    git annex copy --from marcos --to VHS nothere
-
-Thanks for considering this! -- [[anarcat]]
+Recieving to a file, and sending from the same file as it grows is one
+possibility, since that would handle buffering, and it might avoid needing
+to change interfaces as much. It would still need a new interface since the
+current one does not guarantee the file is written in-order.

fix option desc pasted from pull
diff --git a/doc/git-annex-push.mdwn b/doc/git-annex-push.mdwn
index 93fada7826..30b7123a30 100644
--- a/doc/git-annex-push.mdwn
+++ b/doc/git-annex-push.mdwn
@@ -67,7 +67,7 @@ See [[git-annex-preferred-content]](1).
 
 * `--only-annex` `-a`, `--not-only-annex`
 
-  Only pull the git-annex branch and annexed content from remotes,
+  Only push the git-annex branch and annexed content to remotes,
   not other git branches.
 
   The `annex.synconlyannex` configuration can be set to true to make
@@ -75,7 +75,7 @@ See [[git-annex-preferred-content]](1).
   `--not-only-annex`.
 
   When this is combined with --no-content, only the git-annex branch
-  will be pulled.
+  will be pushed.
 
 * `--no-content`, `-g`, `--content`
 

Added a comment: but I'm talking about --help, isn't that in the source code?
diff --git a/doc/bugs/documentation_typo_in_git_annex_find/comment_2_13a5aa9163d1fd392bf52484c6eb4d90._comment b/doc/bugs/documentation_typo_in_git_annex_find/comment_2_13a5aa9163d1fd392bf52484c6eb4d90._comment
new file mode 100644
index 0000000000..6a411ba391
--- /dev/null
+++ b/doc/bugs/documentation_typo_in_git_annex_find/comment_2_13a5aa9163d1fd392bf52484c6eb4d90._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="ErrGe"
+ avatar="http://cdn.libravatar.org/avatar/37423d3bfd69af2cda23d194ec74f991"
+ subject="but I'm talking about --help, isn't that in the source code?"
+ date="2024-04-18T13:07:40Z"
+ content="""
+I'm talking about the output of a --help command, that has to be in the source code somewhere, no?
+"""]]

diff --git a/doc/bugs/Dropping_filename_extensions_with_more_than_4_char.mdwn b/doc/bugs/Dropping_filename_extensions_with_more_than_4_char.mdwn
new file mode 100644
index 0000000000..ef4041a5d3
--- /dev/null
+++ b/doc/bugs/Dropping_filename_extensions_with_more_than_4_char.mdwn
@@ -0,0 +1,54 @@
+### Please describe the problem.
+
+The file extension of annexed files are dropped if they have more than four characters (like `.blend` files)
+All `*E` backends are affected but `WORM` seems to work.
+
+### What steps will reproduce the problem?
+
+[[!format sh """
+mkdir foo && cd foo
+git init
+git annex init
+echo '* annex.backend=SHA256E' > .gitattributes
+echo '* annex.largefiles=(largerthan=0)' >> .gitattributes
+echo 'foo' > foo.abc
+echo 'bar' > bar.abcd
+echo 'baz' > baz.abcde
+echo 'faz' > faz.abcdef
+git annex add .
+ls -l
+"""]]
+
+Outputs
+
+[[!format sh """
+total 16
+lrwxrwxrwx 1 foo bar 188 Apr 18 11:52 bar.abcd -> .git/annex/objects/z2/jk/SHA256E-s4--7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730.abcd/SHA256E-s4--7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730.abcd
+lrwxrwxrwx 1 foo bar 178 Apr 18 11:52 baz.abcde -> .git/annex/objects/MZ/Fq/SHA256E-s4--bf07a7fbb825fc0aae7bf4a1177b2b31fcf8a3feeaf7092761e18c859ee52a9c/SHA256E-s4--bf07a7fbb825fc0aae7bf4a1177b2b31fcf8a3feeaf7092761e18c859ee52a9c
+lrwxrwxrwx 1 foo bar 178 Apr 18 11:52 faz.abcdef -> .git/annex/objects/6Z/zG/SHA256E-s4--0206bf5fc94a74ae22c2c0e93ad1b578ae7f16cb52fb470cddf1f0d324c6bbf3/SHA256E-s4--0206bf5fc94a74ae22c2c0e93ad1b578ae7f16cb52fb470cddf1f0d324c6bbf3
+lrwxrwxrwx 1 foo bar 186 Apr 18 11:52 foo.abc -> .git/annex/objects/Mq/J5/SHA256E-s4--b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c.abc/SHA256E-s4--b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c.abc
+"""]]
+
+
+### What version of git-annex are you using? On what operating system?
+
+- Tested with Ubuntu 22.04
+- git-annex v8 from original ubuntu ppa
+- git-annex-standalone v10 from neurodebian ppa
+
+### Please provide any additional information below.
+
+[[!format sh """
+$ git annex version
+git-annex version: 10.20240227-1~ndall+1
+build flags: Assistant Webapp Pairing Inotify DBus DesktopNotify TorrentParser MagicMime Benchmark Feeds Testsuite S3 WebDAV
+dependency versions: aws-0.22.1 bloomfilter-2.0.1.0 cryptonite-0.29 DAV-1.3.4 feed-1.3.2.1 ghc-9.0.2 http-client-0.7.13.1 persistent-sqlite-2.13.1.0 torrent-10000.1.1 uuid-1.3.15 yesod-1.6.2.1
+key/value backends: SHA256E SHA256 SHA512E SHA512 SHA224E SHA224 SHA384E SHA384 SHA3_256E SHA3_256 SHA3_512E SHA3_512 SHA3_224E SHA3_224 SHA3_384E SHA3_384 SKEIN256E SKEIN256 SKEIN512E SKEIN512 BLAKE2B256E BLAKE2B256 BLAKE2B512E BLAKE2B512 BLAKE2B160E BLAKE2B160 BLAKE2B224E BLAKE2B224 BLAKE2B384E BLAKE2B384 BLAKE2BP512E BLAKE2BP512 BLAKE2S256E BLAKE2S256 BLAKE2S160E BLAKE2S160 BLAKE2S224E BLAKE2S224 BLAKE2SP256E BLAKE2SP256 BLAKE2SP224E BLAKE2SP224 SHA1E SHA1 MD5E MD5 WORM URL X*
+remote types: git gcrypt p2p S3 bup directory rsync web bittorrent webdav adb tahoe glacier ddar git-lfs httpalso borg hook external
+operating system: linux x86_64
+supported repository versions: 8 9 10
+upgrade supported from repository versions: 0 1 2 3 4 5 6 7 8 9 10
+local repository version: 10
+"""]]
+
+

Added a comment: Everyone can fix typos in the docs
diff --git a/doc/bugs/documentation_typo_in_git_annex_find/comment_1_afb7c20d07eb58647d0dba265ee31998._comment b/doc/bugs/documentation_typo_in_git_annex_find/comment_1_afb7c20d07eb58647d0dba265ee31998._comment
new file mode 100644
index 0000000000..93961e05b3
--- /dev/null
+++ b/doc/bugs/documentation_typo_in_git_annex_find/comment_1_afb7c20d07eb58647d0dba265ee31998._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="nobodyinperson"
+ avatar="http://cdn.libravatar.org/avatar/736a41cd4988ede057bae805d000f4f5"
+ subject="Everyone can fix typos in the docs"
+ date="2024-04-18T05:21:42Z"
+ content="""
+Btw you can edit the documentation yourself, just click the EDIT button at the top of the page.
+"""]]

diff --git a/doc/bugs/documentation_typo_in_git_annex_find.mdwn b/doc/bugs/documentation_typo_in_git_annex_find.mdwn
new file mode 100644
index 0000000000..cee770d3ac
--- /dev/null
+++ b/doc/bugs/documentation_typo_in_git_annex_find.mdwn
@@ -0,0 +1,15 @@
+### Please describe the problem.
+
+`git annex find --help` incorrectly documents `--copies REMOTE`, the REMOTE should be NUMBER.
+
+### What steps will reproduce the problem?
+
+`git annex find --help | grep -- --copies`
+
+### What version of git-annex are you using? On what operating system?
+
+10.20240129
+
+### Have you had any luck using git-annex before? (Sometimes we get tired of reading bug reports all day and a lil' positive end note does wonders)
+
+git annex is awesome, was always awesome and will always be awesome.  I appreciate all the work going into it.

Added a comment: hook idea implementation is cool, but usage is not so simple for the enduser
diff --git a/doc/todo/force_star_topology_on_a_repository/comment_8_f840a405028df2630a7c8eac5091833f._comment b/doc/todo/force_star_topology_on_a_repository/comment_8_f840a405028df2630a7c8eac5091833f._comment
new file mode 100644
index 0000000000..d54aa94285
--- /dev/null
+++ b/doc/todo/force_star_topology_on_a_repository/comment_8_f840a405028df2630a7c8eac5091833f._comment
@@ -0,0 +1,18 @@
+[[!comment format=mdwn
+ username="ErrGe"
+ avatar="http://cdn.libravatar.org/avatar/37423d3bfd69af2cda23d194ec74f991"
+ subject="hook idea implementation is cool, but usage is not so simple for the enduser"
+ date="2024-04-18T01:17:02Z"
+ content="""
+Sorry for resurrecting this after 2 years, I somehow forgot this discussion was ongoing.
+
+So, first of all, thank you so much for taking the time to writing up a very cool server side solution for the problem.  Do I understand your proposal correctly, that basically on the server we would always store a git-annex rewritten branch as if it was correctly written by the client, no matter what the clients do on their own in their own git-annex branches, right?
+
+And since all the merging in git-annex is line based, this constant rewrite wouldn't confuse the clients when they `git fetch --all` + `git annex merge`?  Wouldn't the merge commits in `gitk git-annex` be very weird to understand?
+
+So what I don't understand, is that if we do this on the central server side, then yes, the rewrite on the server is good, but when the offending client does a `git fetch` + `git annex merge`, it will create a merge commit with 2 parents.  Will we also straighten that out automatically and delete the \"stupid\" side on the next push?  Doesn't this mean, that debugging just becomes more confusing and this client will create longer and longer side branches on its graphical branch view of `gitk git-annex`?
+
+Let me reflect back to your \"comment 5\", where you asked the very valid question of what to do in case of difference of opinions.  I think the correct solution is to implement the override feature (in .git/config, as you said), and let it completely happen.  If the only way for unwanted UUIDs to appear in my central repo is for someone to use this extra feature, I'm OK with that.  I want to prevent accidents, and I certainly don't want to prevent expert power-users achieving their goals when needed, so local override (even if the end result is pushed back), is 100% fine.
+
+Now, that I'm thinking about this as a \"reasonable difference of opinion to have\", an interesting \"solution\" comes to mind, that opens up of course a very big discussion: why in the design of git-annex there is only ONE AND ONLY git-annex branch?  Git has orphan branches, and it would be legit to say, that different group of people working in a repo, have different opinion of \"view of the annex\", e.g. they think different repos (or special remotes) are important or unimportant for them. I mention this question not really seriously as a proposal to redesign, but I'm sure that you had this idea sometime in the past, and if you have some insight or revelations, I'd be happy to read it.
+"""]]

update
diff --git a/doc/thanks/list b/doc/thanks/list
index 4a1178473d..2ebbdf39a5 100644
--- a/doc/thanks/list
+++ b/doc/thanks/list
@@ -116,3 +116,4 @@ kk,
 Jaime Marquínez Ferrándiz, 
 Stephen Seo, 
 Antoine Balaine, 
+mycroft, 

rclone special remote
Added rclone special remote, which can be used without needing to install
the git-annex-remote-rclone program. This needs a new version of rclone,
which supports "rclone gitannex".
This is implemented as a variant of an external special remote, that
runs "rclone gitannex" instead of the usual git-annex-remote- command.
Parameterized Remote.External to support that.
Sponsored-by: Luke T. Shumaker on Patreon
diff --git a/CHANGELOG b/CHANGELOG
index 3a4adca108..5e73fd0dac 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -15,6 +15,9 @@ git-annex (10.20240228) UNRELEASED; urgency=medium
     versions of MinTTY.
   * sync, assist, import: Allow -m option to be specified multiple
     times, to provide additional paragraphs for the commit message.
+  * Added rclone special remote, which can be used without needing
+    to install the git-annex-remote-rclone program. This needs
+    a new version of rclone, which supports "rclone gitannex".
 
  -- Joey Hess <id@joeyh.name>  Tue, 27 Feb 2024 13:07:10 -0400
 
diff --git a/Remote/External.hs b/Remote/External.hs
index 179043c3bc..a974ad126c 100644
--- a/Remote/External.hs
+++ b/Remote/External.hs
@@ -9,7 +9,7 @@
 {-# LANGUAGE BangPatterns #-}
 {-# LANGUAGE RankNTypes #-}
 
-module Remote.External (remote) where
+module Remote.External where
 
 import Remote.External.Types
 import Remote.External.AsyncExtension
@@ -48,10 +48,10 @@ remote :: RemoteType
 remote = specialRemoteType $ RemoteType
 	{ typename = "external"
 	, enumerate = const (findSpecialRemotes "externaltype")
-	, generate = gen
-	, configParser = remoteConfigParser
-	, setup = externalSetup
-	, exportSupported = checkExportSupported
+	, generate = gen remote Nothing
+	, configParser = remoteConfigParser Nothing
+	, setup = externalSetup Nothing Nothing
+	, exportSupported = checkExportSupported Nothing
 	, importSupported = importUnsupported
 	, thirdPartyPopulated = False
 	}
@@ -62,15 +62,15 @@ externaltypeField = Accepted "externaltype"
 readonlyField :: RemoteConfigField
 readonlyField = Accepted "readonly"
 
-gen :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
-gen r u rc gc rs
+gen :: RemoteType -> Maybe ExternalProgram -> Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
+gen rt externalprogram r u rc gc rs
 	-- readonly mode only downloads urls; does not use external program
-	| externaltype == "readonly" = do
+	| externalprogram' == ExternalType "readonly" = do
 		c <- parsedRemoteConfig remote rc
 		cst <- remoteCost gc c expensiveRemoteCost
 		let rmt = mk c cst (pure GloballyAvailable)
 			Nothing
-			(externalInfo externaltype)
+			(externalInfo externalprogram')
 			Nothing
 			Nothing
 			exportUnsupported
@@ -83,7 +83,7 @@ gen r u rc gc rs
 			rmt
 	| otherwise = do
 		c <- parsedRemoteConfig remote rc
-		external <- newExternal externaltype (Just u) c (Just gc)
+		external <- newExternal externalprogram' (Just u) c (Just gc)
 			(Git.remoteName r) (Just rs)
 		Annex.addCleanupAction (RemoteCleanup u) $ stopExternal external
 		cst <- getCost external r gc c
@@ -150,19 +150,27 @@ gen r u rc gc rs
 			, appendonly = False
 			, untrustworthy = False
 			, availability = avail
-			, remotetype = remote 
+			, remotetype = rt 
 				{ exportSupported = cheapexportsupported }
-			, mkUnavailable = gen r u rc
-				(gc { remoteAnnexExternalType = Just "!dne!" }) rs
+			, mkUnavailable =
+				let dneprogram = case externalprogram of
+					Just (ExternalCommand _ _) -> Just (ExternalType "!dne!")
+					_ -> Nothing
+				    dnegc = gc { remoteAnnexExternalType = Just "!dne!" }
+				in gen rt dneprogram r u rc dnegc rs
 			, getInfo = togetinfo
 			, claimUrl = toclaimurl
 			, checkUrl = tocheckurl
 			, remoteStateHandle = rs
 			}
-	externaltype = fromMaybe (giveup "missing externaltype") (remoteAnnexExternalType gc)
-
-externalSetup :: SetupStage -> Maybe UUID -> Maybe CredPair -> RemoteConfig -> RemoteGitConfig -> Annex (RemoteConfig, UUID)
-externalSetup _ mu _ c gc = do
+	externalprogram' = case externalprogram of
+		Just p -> p
+		Nothing -> ExternalType $ 
+			fromMaybe (giveup "missing externaltype")
+				(remoteAnnexExternalType gc)
+
+externalSetup :: Maybe ExternalProgram -> Maybe (String, String) -> SetupStage -> Maybe UUID -> Maybe CredPair -> RemoteConfig -> RemoteGitConfig -> Annex (RemoteConfig, UUID)
+externalSetup externalprogram setgitconfig _ mu _ c gc = do
 	u <- maybe (liftIO genUUID) return mu
 	pc <- either giveup return $ parseRemoteConfig c lenientRemoteConfigParser
 	let readonlyconfig = getRemoteConfigValue readonlyField pc == Just True
@@ -182,7 +190,8 @@ externalSetup _ mu _ c gc = do
 			return c'
 		else do
 			pc' <- either giveup return $ parseRemoteConfig c' lenientRemoteConfigParser
-			external <- newExternal externaltype (Just u) pc' (Just gc) Nothing Nothing
+			let p = fromMaybe (ExternalType externaltype) externalprogram
+			external <- newExternal p (Just u) pc' (Just gc) Nothing Nothing
 			-- Now that we have an external, ask it to LISTCONFIGS, 
 			-- and re-parse the RemoteConfig strictly, so we can
 			-- error out if the user provided an unexpected config.
@@ -200,17 +209,20 @@ externalSetup _ mu _ c gc = do
 				liftIO . atomically . readTMVar . externalConfigChanges
 			return (changes c')
 
-	gitConfigSpecialRemote u c'' [("externaltype", externaltype)]
+	gitConfigSpecialRemote u c''
+		[ fromMaybe ("externaltype", externaltype) setgitconfig ]
 	return (M.delete readonlyField c'', u)
 
-checkExportSupported :: ParsedRemoteConfig -> RemoteGitConfig -> Annex Bool
-checkExportSupported c gc = do
+checkExportSupported :: Maybe ExternalProgram -> ParsedRemoteConfig -> RemoteGitConfig -> Annex Bool
+checkExportSupported Nothing c gc = do
 	let externaltype = fromMaybe (giveup "Specify externaltype=") $
 		remoteAnnexExternalType gc <|> getRemoteConfigValue externaltypeField c
 	if externaltype == "readonly"
 		then return False
-		else checkExportSupported' 
-			=<< newExternal externaltype Nothing c (Just gc) Nothing Nothing
+		else checkExportSupported (Just (ExternalType externaltype)) c gc
+checkExportSupported (Just externalprogram) c gc = 
+	checkExportSupported' 
+		=<< newExternal externalprogram Nothing c (Just gc) Nothing Nothing
 
 checkExportSupported' :: External -> Annex Bool
 checkExportSupported' external = go `catchNonAsync` (const (return False))
@@ -658,7 +670,7 @@ startExternal' external = do
 		n <- succ <$> readTVar (externalLastPid external)
 		writeTVar (externalLastPid external) n
 		return n
-	AddonProcess.startExternalAddonProcess basecmd [] pid >>= \case
+	AddonProcess.startExternalAddonProcess externalcmd externalparams pid >>= \case
 		Left (AddonProcess.ProgramFailure err) -> do
 			unusable err
 		Left (AddonProcess.ProgramNotInstalled err) ->
@@ -667,7 +679,7 @@ startExternal' external = do
 					[ err
 					, "This remote has annex-readonly=true, and previous versions of"
 					, "git-annex would try to download from it without"
-					, "installing " ++ basecmd ++ ". If you want that, you need to set:"
+					, "installing " ++ externalcmd ++ ". If you want that, you need to set:"
 					, "git config remote." ++ rname ++ ".annex-externaltype readonly"
 					]
 				_ -> unusable err
@@ -686,7 +698,9 @@ startExternal' external = do
 			extensions <- startproto st
 			return (st, extensions)
   where
-	basecmd = "git-annex-remote-" ++ externalType external
+	(externalcmd, externalparams) = case externalProgram external of
+		ExternalType t -> ("git-annex-remote-" ++ t, [])
+		ExternalCommand c ps -> (c, ps)
 	startproto st = do
 		receiveMessage st external
 			(const Nothing)
@@ -707,13 +721,13 @@ startExternal' external = do
 		case filter (`notElem` fromExtensionList supportedExtensionList) (fromExtensionList exwanted) of
 			[] -> return exwanted
 			exrest -> unusable $ unwords $
-				[ basecmd
+				[ externalcmd
 				, "requested extensions that this version of git-annex does not support:"
 				] ++ exrest
 
 	unusable msg = do
 		warning (UnquotedString msg)
-		giveup ("unable to use external special remote " ++ basecmd)
+		giveup ("unable to use external special remote " ++ externalcmd)
 
 stopExternal :: External -> Annex ()
 stopExternal external = liftIO $ do
@@ -825,12 +839,13 @@ getWebUrls key = filter supported <$> getUrls key
   where
 	supported u = snd (getDownloader u) == WebDownloader
 			
-externalInfo :: ExternalType -> Annex [(String, String)]
-externalInfo et = return [("externaltype", et)]
+externalInfo :: ExternalProgram -> Annex [(String, String)]
+externalInfo (ExternalType et) = return [("externaltype", et)]
+externalInfo (ExternalCommand _ _) = return []
 
 getInfoM :: External -> Annex [(String, String)]
 getInfoM external = (++)
-	<$> externalInfo (externalType external)

(Diff truncated)
update
diff --git a/doc/todo/external_special_remotes_not_using_git-annex-remote_in_name.mdwn b/doc/todo/external_special_remotes_not_using_git-annex-remote_in_name.mdwn
index 5083acc733..c21aebe88e 100644
--- a/doc/todo/external_special_remotes_not_using_git-annex-remote_in_name.mdwn
+++ b/doc/todo/external_special_remotes_not_using_git-annex-remote_in_name.mdwn
@@ -1,11 +1,12 @@
 rclone now supports being run as a git-annex special remote natively
 see <https://github.com/rclone/rclone/pull/7654>. "rclone gitannex"
 is the command to run. But git-annex needs a git-annex-remote-rclone or similar,
-so they are shipping a git-annex-remote-rclone-builtin symlink to rclone,
+so to use it needs a git-annex-remote-rclone-builtin symlink to rclone,
 and when run under that name it behaves as if "rclone gitannex" were run.
 
-So in this case, the need for "git-annex-remote-foo" is complicating an
-upstream project that has gone out of its way to support git-annex. Not ideal.
+rclone won't be shipping that symlink, so users will have to create it
+themselves, which complicates using it. So could git-annex instead
+support running "rclone gitannex" itself?
 
 From the pull request, @dmcardle wrote:
 
@@ -27,3 +28,19 @@ run "rclone gitannex".
 Or, git-annex add an internal rclone special remote, that is just
 a wrapper around the external special remote, that makes it use
 "rclone gitannex". "git-annex initremote foo type=rclone" --[[Joey]]
+
+> Considering those two approaches again, the first would let the new
+> rclone be used with old git-annex with the user making the
+> git-annex-remote-rclone-builtin symlink. Then later, that same
+> special remote could be used with the new git-annex on a system
+> where that symlink was not set up.
+> 
+> The second approach would make a special remote that can't be used with
+> old git-annex. But if the user wanted to use the new rclone with old
+> git-annex, they can still make the symlink. To later migrate away from
+> the symlink, they would need to initremote another special remote using
+> --sameas.
+>
+> I feel that the simplicity of the type=rclone config will pay off in the
+> long term, vs short term complication for probably a small subset of users
+> who somehow can upgrade rclone but can't upgrade git-annex. --[[Joey]]

bug about inception with unlocked files
diff --git a/doc/bugs/assistant___40__webapp__41___commited_unlocked_link_to_annex.mdwn b/doc/bugs/assistant___40__webapp__41___commited_unlocked_link_to_annex.mdwn
new file mode 100644
index 0000000000..1b084030c1
--- /dev/null
+++ b/doc/bugs/assistant___40__webapp__41___commited_unlocked_link_to_annex.mdwn
@@ -0,0 +1,106 @@
+### Please describe the problem.
+
+Today I noticed odd commits happening such as
+
+```
+❯ git show 4a157861f3d27a40b38ae441dfe306e45e448c66
+commit 4a157861f3d27a40b38ae441dfe306e45e448c66
+Author: ReproStim User <changeme@example.com>
+Date:   Wed Apr 17 09:22:04 2024 -0400
+
+    git-annex in reprostim@reproiner:/data/reprostim
+
+diff --git a/Videos/2024.03.17.14.09.12.550_2024.03.17.14.09.18.818.mkv.log b/Videos/2024.03.17.14.09.12.550_2024.03.17.14.09.18.818.mkv.log
+index fc930f54..92b79020 100644
+--- a/Videos/2024.03.17.14.09.12.550_2024.03.17.14.09.18.818.mkv.log
++++ b/Videos/2024.03.17.14.09.12.550_2024.03.17.14.09.18.818.mkv.log
+@@ -1 +1 @@
+-/annex/objects/MD5E-s68799--29541299bea3691f430d855d2fb432fb.mkv.log
++/annex/objects/MD5E-s69--08983cc11522233e5d4815e4ef62275a.mkv.log
+```
+
+-- today is April but commits are for files in March... 
+
+There is `git annex webapp` running which is configured to offload all content to another host.
+
+And actual patch shows that it pretty much annexed the "unlocked link" file after the file was offloaded to remote host.
+
+
+Do not have a minimal reproducer yet, but I think it happened while
+
+- I had initially .log files which are text going to git
+- then I added to `.gitattributes` 
+
+```
+*.log annex.largefiles=anything
+```
+
+but it was never committed (? I assumed that annex webapp/assistant would do that -- it didn't) -- only now I did that.  
+- not sure how this morning was special...
+
+The most interesting is that if I `annex get` -- I do get correct file...
+
+It is like an inception!!!
+
+On the fresh clone, if I look inside that file I see short key:
+
+```
+❯ cat 2024.03.17.14.09.12.550_2024.03.17.14.09.18.818.mkv.log
+/annex/objects/MD5E-s69--08983cc11522233e5d4815e4ef62275a.mkv.log
+```
+
+then, if I `annex get` it -- I get content with long key
+
+```shell
+❯ git annex get 2024.03.17.14.09.12.550_2024.03.17.14.09.18.818.mkv.log
+get 2024.03.17.14.09.12.550_2024.03.17.14.09.18.818.mkv.log (from rolando...) 
+ok                                
+(recording state in git...)
+❯ cat 2024.03.17.14.09.12.550_2024.03.17.14.09.18.818.mkv.log
+/annex/objects/MD5E-s68799--29541299bea3691f430d855d2fb432fb.mkv.log
+```
+
+then upon subsequent get -- I will get the actual content:
+
+```shell
+❯ git annex get 2024.03.17.14.09.12.550_2024.03.17.14.09.18.818.mkv.log
+get 2024.03.17.14.09.12.550_2024.03.17.14.09.18.818.mkv.log (from rolando...) 
+ok                                
+(recording state in git...)
+❯ head -n 1 2024.03.17.14.09.12.550_2024.03.17.14.09.18.818.mkv.log
+2024-03-17 14:09:12.551 [info] [685899] Session logging begin   : reprostim-videocapture 1.5.0.119, session_logger_2024.03.17.14.09.12.550, start_ts=2024.03.17.14.09.12.550
+```
+and dropping it would lead me just to the "long key"
+
+```
+❯ git annex drop 2024.03.17.14.09.12.550_2024.03.17.14.09.18.818.mkv.log
+drop 2024.03.17.14.09.12.550_2024.03.17.14.09.18.818.mkv.log (locking rolando...) ok
+(recording state in git...)
+❯ cat 2024.03.17.14.09.12.550_2024.03.17.14.09.18.818.mkv.log
+/annex/objects/MD5E-s68799--29541299bea3691f430d855d2fb432fb.mkv.log
+```
+
+and will not be able to come out into reality from the 2nd level of inception:
+
+```
+❯ git annex drop 2024.03.17.14.09.12.550_2024.03.17.14.09.18.818.mkv.log
+❯ cat 2024.03.17.14.09.12.550_2024.03.17.14.09.18.818.mkv.log
+/annex/objects/MD5E-s68799--29541299bea3691f430d855d2fb432fb.mkv.log
+```
+
+
+### What version of git-annex are you using? On what operating system?
+
+on original server with webapp:  10.20240227-1~ndall+1
+
+on intermediate server through which transfer of files happens: I think it might be old 
+
+```
+[bids@rolando VIDS] > git annex version
+git-annex version: 6.20180808-ga1327779a
+```
+
+on laptop where I dive into inception: 10.20240129
+
+[[!meta author=yoh]]
+[[!tag projects/repronim]]

original possible todo on extension
diff --git a/doc/todo/way_to_instruct_on_how_to_decide_on_extension__63__.mdwn b/doc/todo/way_to_instruct_on_how_to_decide_on_extension__63__.mdwn
new file mode 100644
index 0000000000..83e957c8a8
--- /dev/null
+++ b/doc/todo/way_to_instruct_on_how_to_decide_on_extension__63__.mdwn
@@ -0,0 +1,14 @@
+In our case we are storing videos using timestamp in the filename, e.g.
+
+```
+2024.03.08.09.31.09.041_2024.03.08.09.34.53.759.mkv
+```
+
+where last number is `ms`.  `git-annex` for MD5E decides that extension is `.759.mkv`, so if we rename file (adjust timing), it seems to produce a new key.
+
+I wonder if you have any ideas Joey on how to overcome it (smarter extension deduction? some config to "hardcode" target extension to be .mkv?)?
+
+Just throwing against the wall to see if sticks
+
+[[!meta author=yoh]]
+[[!tag projects/repronim]]

Added a comment: Need for more than HEAD/URL?
diff --git a/doc/todo/compute_special_remote/comment_2_bdb9c77b3ac97cef8d1b8eeaaf300d8b._comment b/doc/todo/compute_special_remote/comment_2_bdb9c77b3ac97cef8d1b8eeaaf300d8b._comment
new file mode 100644
index 0000000000..34d518f309
--- /dev/null
+++ b/doc/todo/compute_special_remote/comment_2_bdb9c77b3ac97cef8d1b8eeaaf300d8b._comment
@@ -0,0 +1,18 @@
+[[!comment format=mdwn
+ username="mih"
+ avatar="http://cdn.libravatar.org/avatar/f881df265a423e4f24eff27c623148fd"
+ subject="Need for more than HEAD/URL?"
+ date="2024-04-15T05:00:58Z"
+ content="""
+> \"Instruction deposition\" is essentially just adding a URL to a key in my implementation, which is pretty nice. Using the built-in relaxed option automatically gives the distinction between generating keys that have never existed and regenerating keys.
+
+Thanks for the pointer, very useful!
+
+Regarding the points you raised:
+
+Datalad's `run` feature has been around for some years, and we have seen usage in the wild with command lines that are small programs and dozens, sometimes hundreds of inputs. It is true that anything could be simply URL-encoded. However, especially with command-patterns (always same, except parameter change) that may be needlessly heavy. Maybe it would compress well (likely), but it still poses a maintenance issue. Say the compute instructions need an update (software API change): Updating one shared instruction set is a simpler task than sifting through annex-keys and rewriting URLs.
+
+> I don't quite understand the necessity for \"Worktree provisioning\". If I understand that right, I think it would just make things more complicated and unintuitive compared to always staying in HEAD.
+
+We need a worktree different from `HEAD` whenever HEAD has changed from the original worktree used for setting up a compute instruction. Say a command needs two input files, but one has been moved to a different directory in current `HEAD`. An implementation would now either say \"no longer available\" and force maintenance update, or be able to provision the respective worktree. In case of no provision capability we would need to replace the URL-encoded instructions (this would make the key uncomputable in earlier versions), or amend with an additional instruction set (and now we would start to accumulate cruft where changes in the git-annex branch need to account for (unrelated) changes in any other branch).
+"""]]

Added a comment: prior art
diff --git a/doc/todo/compute_special_remote/comment_1_9f4835cd08d9d02009b685f4a366a245._comment b/doc/todo/compute_special_remote/comment_1_9f4835cd08d9d02009b685f4a366a245._comment
new file mode 100644
index 0000000000..539e311fff
--- /dev/null
+++ b/doc/todo/compute_special_remote/comment_1_9f4835cd08d9d02009b685f4a366a245._comment
@@ -0,0 +1,15 @@
+[[!comment format=mdwn
+ username="m.risse@77eac2c22d673d5f10305c0bade738ad74055f92"
+ nickname="m.risse"
+ avatar="http://cdn.libravatar.org/avatar/59541f50d845e5f81aff06e88a38b9de"
+ subject="prior art"
+ date="2024-04-13T20:30:56Z"
+ content="""
+I just want to mention that I've implemented/tried to implement something like this in <https://github.com/matrss/datalad-getexec>. It basically just records a command line invocation to execute and all required input files as base64-encoded json in a URL with a custom scheme, which made it surprisingly simple to implement. I haven't touched it in a while and it was more of an experiment, but other than issues with dependencies on files in sub-datasets it worked pretty well. The main motivation to build it was the mentioned use-case of automatically converting between file formats. Of course it doesn't address all of your mentioned points. E.g. trust is something I haven't considered in my experiments, at all. But it shows that the current special remote protocol is sufficient for a basic implementation of this.
+
+I like the proposed \"request one key, receive many\" extension to the special remote protocol and I think that could be useful in other \"unusual\" special remotes as well.
+
+I don't quite understand the necessity for \"Worktree provisioning\". If I understand that right, I think it would just make things more complicated and unintuitive compared to always staying in HEAD.
+
+\"Instruction deposition\" is essentially just adding a URL to a key in my implementation, which is pretty nice. Using the built-in relaxed option automatically gives the distinction between generating keys that have never existed and regenerating keys.
+"""]]

diff --git a/doc/forum/How_to_allow_clones_to_get_files_via_URL__63__.mdwn b/doc/forum/How_to_allow_clones_to_get_files_via_URL__63__.mdwn
index ad7396f7b7..f3978d8123 100644
--- a/doc/forum/How_to_allow_clones_to_get_files_via_URL__63__.mdwn
+++ b/doc/forum/How_to_allow_clones_to_get_files_via_URL__63__.mdwn
@@ -5,6 +5,7 @@ I’ve been trying a set up a dataset that primarily lives on a web server, but
 I used `datalad addurls` to add the URL of each file on the server to each file in the annex. When I run `git annex whereis filename`, it shows up that it lives on the server in the server’s local copy of the dataset, and that it lives on the web, with a correct URL. In fact, if I click on that URL and open it in a browser, it downloads my file.
 
 The dataset lives on Github, but the annex does not. When I make a clone of the superdataset on my personal computer, I get messages like
+
 ```
 [INFO   ] Unable to parse git config from origin                                                                                       
 [INFO   ] Remote origin does not have git-annex installed; setting annex-ignore                                                        
@@ -14,6 +15,7 @@ install(ok): /home/erin/Documents/DHA/carcas (dataset)
 
 Then when I'm in the dataset `carcas-models` that has the annex and I run `datalad get models
 /Alpaca\ 3rd\ Carpal\ L.glb`, I get this error message: 
+
 ```
 get(error): models/Alpaca 3rd Carpal L.glb (file) [no known url                                
 no known url
@@ -21,6 +23,7 @@ no known url]
 ```
 
 I suspect my problem is with how I set things up with git annex, because when I try `git annex get models/Alpaca\ 3rd\ Carpal\ L.glb`, I get the error:
+
 ```
 get models/Alpaca 3rd Carpal L.glb (from web...) 
   no known url

diff --git a/doc/forum/How_to_allow_clones_to_get_files_via_URL__63__.mdwn b/doc/forum/How_to_allow_clones_to_get_files_via_URL__63__.mdwn
new file mode 100644
index 0000000000..ad7396f7b7
--- /dev/null
+++ b/doc/forum/How_to_allow_clones_to_get_files_via_URL__63__.mdwn
@@ -0,0 +1,49 @@
+I'm working with Datalad, but I suspect that my problems stem from not fully understanding how git annex works.
+
+I’ve been trying a set up a dataset that primarily lives on a web server, but needs to be clone-able by other people. The annex files are visible and downloadable from the server’s website. In particular, the files I’m concerned about here are in a subdataset.
+
+I used `datalad addurls` to add the URL of each file on the server to each file in the annex. When I run `git annex whereis filename`, it shows up that it lives on the server in the server’s local copy of the dataset, and that it lives on the web, with a correct URL. In fact, if I click on that URL and open it in a browser, it downloads my file.
+
+The dataset lives on Github, but the annex does not. When I make a clone of the superdataset on my personal computer, I get messages like
+```
+[INFO   ] Unable to parse git config from origin                                                                                       
+[INFO   ] Remote origin does not have git-annex installed; setting annex-ignore                                                        
+|   This could be a problem with the git-annex installation on the remote. Please make sure that git-annex-shell is available in PATH when you ssh into the remote. Once you have fixed the git-annex installation, run: git annex enableremote origin 
+install(ok): /home/erin/Documents/DHA/carcas (dataset)
+```
+
+Then when I'm in the dataset `carcas-models` that has the annex and I run `datalad get models
+/Alpaca\ 3rd\ Carpal\ L.glb`, I get this error message: 
+```
+get(error): models/Alpaca 3rd Carpal L.glb (file) [no known url                                
+no known url
+no known url]
+```
+
+I suspect my problem is with how I set things up with git annex, because when I try `git annex get models/Alpaca\ 3rd\ Carpal\ L.glb`, I get the error:
+```
+get models/Alpaca 3rd Carpal L.glb (from web...) 
+  no known url
+
+  Unable to access these remotes: web
+
+  Maybe add some of these git remotes (git remote add ...):
+        095e299d-037e-4172-87e0-bbd7183a6613 -- CARCAS models on the 3dviewers server
+
+  (Note that these git remotes have annex-ignore set: origin)
+failed
+get: 1 failed
+```
+
+I'm confused on how to debug this because when I run git annex whereis models/Alpaca\ 3rd\ Carpal\ L.glb, everything looks correct:
+
+```
+whereis models/Alpaca 3rd Carpal L.glb (2 copies) 
+        00000000-0000-0000-0000-000000000001 -- web
+        095e299d-037e-4172-87e0-bbd7183a6613 -- CARCAS models on the 3dviewers server
+
+  web: https://3dviewer.sites.carleton.edu/carcas/carcas-models/models/Alpaca%203rd%20Carpal%20L.glb
+ok
+```
+
+What's the correct way to set up this use case? I don't think that I want the server to be a special remote, because the hidden files like .gitattributes aren't visible. I want to be able to put more files on the server, add their URLS based on where they are on the server, and push to github so that other people can get these files if they want. 

Added a comment: Happens on nix on MacOS as well
diff --git a/doc/bugs/test__58___posix__95__spawnp_broken_10_on_darwin/comment_2_3fb2b47ade727a1bc6a99120b68d98b7._comment b/doc/bugs/test__58___posix__95__spawnp_broken_10_on_darwin/comment_2_3fb2b47ade727a1bc6a99120b68d98b7._comment
new file mode 100644
index 0000000000..de30ce0b65
--- /dev/null
+++ b/doc/bugs/test__58___posix__95__spawnp_broken_10_on_darwin/comment_2_3fb2b47ade727a1bc6a99120b68d98b7._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="nobodyinperson"
+ avatar="http://cdn.libravatar.org/avatar/736a41cd4988ede057bae805d000f4f5"
+ subject="Happens on nix on MacOS as well"
+ date="2024-04-13T15:51:54Z"
+ content="""
+I just had the same problem. An `addurl` test failing while building on MacOS with `nix-shell -p git-annex`. I recorded a video of it coincidentally. I don't know what's with the `security` program it tries to call. I was using a MacOS VM via [Docker-OSX](https://github.com/sickcodes/Docker-OSX?tab=readme-ov-file#big-sur-).
+"""]]

diff --git a/doc/bugs/Invalid_option___96__--time-limit__61__1m__39___with_pull.mdwn b/doc/bugs/Invalid_option___96__--time-limit__61__1m__39___with_pull.mdwn
new file mode 100644
index 0000000000..42f42f366a
--- /dev/null
+++ b/doc/bugs/Invalid_option___96__--time-limit__61__1m__39___with_pull.mdwn
@@ -0,0 +1,46 @@
+### Please describe the problem.
+
+git annex pull reports `--time-limit=1m` as an invalid option, even though its manpage states that it can use the options from git-annex-common-options and the manpage for those includes the --time-limit option.
+
+
+### What steps will reproduce the problem?
+
+git annex pull while specifying the --time-limit option.
+
+
+### What version of git-annex are you using? On what operating system?
+
+[[!format sh """
+$ git annex version
+git-annex version: 10.20240227
+build flags: Assistant Webapp Pairing Inotify DBus DesktopNotify TorrentParser MagicMime Feeds Testsuite S3 WebDAV
+dependency versions: aws-0.24.1 bloomfilter-2.0.1.2 crypton-0.34 DAV-1.3.4 feed-1.3.2.1 ghc-9.6.4 http-client-0.7.17 persistent-sqlite-2.13.3.0 torrent-10000.1.3 uuid-1.3.15 yesod-1.6.2.1
+key/value backends: SHA256E SHA256 SHA512E SHA512 SHA224E SHA224 SHA384E SHA384 SHA3_256E SHA3_256 SHA3_512E SHA3_512 SHA3_224E SHA3_224 SHA3_384E SHA3_384 SKEIN256E SKEIN256 SKEIN512E SKEIN512 BLAKE2B256E BLAKE2B256 BLAKE2B512E BLAKE2B512 BLAKE2B160E BLAKE2B160 BLAKE2B224E BLAKE2B224 BLAKE2B384E BLAKE2B384 BLAKE2BP512E BLAKE2BP512 BLAKE2S256E BLAKE2S256 BLAKE2S160E BLAKE2S160 BLAKE2S224E BLAKE2S224 BLAKE2SP256E BLAKE2SP256 BLAKE2SP224E BLAKE2SP224 SHA1E SHA1 MD5E MD5 WORM URL X*
+remote types: git gcrypt p2p S3 bup directory rsync web bittorrent webdav adb tahoe glacier ddar git-lfs httpalso borg hook external
+operating system: linux x86_64
+supported repository versions: 8 9 10
+upgrade supported from repository versions: 0 1 2 3 4 5 6 7 8 9 10
+local repository version: 10
+"""]]
+
+On Ubuntu, but git-annex is installed from a recent nixpkgs.
+
+
+### Please provide any additional information below.
+
+[[!format sh """
+# If you can, paste a complete transcript of the problem occurring here.
+# If the problem is with the git-annex assistant, paste in .git/annex/daemon.log
+
+$ git annex pull --time-limit=1m
+Invalid option `--time-limit=1m'
+
+Usage: git-annex COMMAND
+[...]
+
+# End of transcript or log.
+"""]]
+
+### Have you had any luck using git-annex before? (Sometimes we get tired of reading bug reports all day and a lil' positive end note does wonders)
+
+I am currently working on a web interface that lets non-git-annex users request files to be available in a specific repository (which will be located on the storage cluster of a HPC system). A combination of git annex metadata and an appropriate required content expression should make this essentially trivial, which is nice. The time-limit option would be helpful for the background worker doing all the fetching, though.

Thoughts on support special remotes that compute keys instead of downloading
diff --git a/doc/todo/compute_special_remote.mdwn b/doc/todo/compute_special_remote.mdwn
new file mode 100644
index 0000000000..a4c57c0b27
--- /dev/null
+++ b/doc/todo/compute_special_remote.mdwn
@@ -0,0 +1,62 @@
+# Enable git-annex to provision file content by other means than download
+
+This idea [goes back many years](https://github.com/datalad/datalad/issues/2850), and has been [iterated on repeatedly afterwards](https://github.com/datalad/datalad-next/issues/143), and most recently at Distribits 2024.
+The following is a summary of what role git-annex could play in this functionality.
+
+The basic idea is to wrap a provision-by-compute process into the standard interface of a git annex remote.
+A consumer would (theoretically) not need to worry about how an annex key is provided, they would simply get-annex-get it, whether this leads to a download or a computation.
+Moreover, access cost and redundancies could be managed/communicated using established patterns.
+
+## Use cases
+
+Here are a few concrete use cases that illustrate why one would want to have functionality like this
+
+### Generate annex keys (that have never existed)
+
+This can be useful for leaving instructions how, e.g. other data formats can be generated from a single format that is kept on storage.
+For example, a collection of CSV files is stored, but an XLSX variant can be generated upon request automatically.
+Or a single large live stream video is stored, and a collection of shorter clips is generated from a cue sheet or cut list.
+
+
+### Re-generate annex keys
+
+This can be useful when storing a key is expensive, but its exact identity is known/important. For example, an outcome of a scientific computation yields a large output that is expensive to compute and to store, yet needs to be tracked for repeated further processing -- the cost of a recomputation may be reduced, by storing (smaller) intermediate results, and leaving instruction how to perform (a different) computation that yields the identical original output.
+
+This second scenario, where annex keys are reproduced exactly, can be considered the general case. It generally requires exact inputs to the computation, where as the first scenario can/should handle an application of a compute instruction on any compatible input data.
+
+
+## What is in scope for git-annex?
+
+The execution of arbitrary code without any inherent trust is a problem. A problem that git-annex may not want to get into. Moreover, there are many candidate environments for code execution -- a complexity that git-annex may not want to get into either.
+
+### External remote protocol sufficient?
+
+From my point of view, pretty much all required functionality could be hidden behind the external remote protocol and thereby inside on or more special remote implementations.
+
+- `STORE`: somehow capture the computing instructions, likely linking some code to some (key-specific) parameters, like input files
+- `CHECKPRESENT`: do compute instruction for a key exist?
+- `RETRIEVE`: compute the key
+- `REMOVE`: remove the instructions/parameter record
+- `WHEREIS`: give information on computation/inputs
+
+where `SET/GETSTATE` may implement the instruction deposit/retrieval.
+
+### Worktree provisioning?
+
+Such external remote implementation would need a way to create suitable worktrees to (re)run a given code. Git-annex support to provide a (separate) worktree for a repository at a specific commit, with efficient (re)use of the main repository's annex would simplify such implementations.
+
+### Request one key, receive many
+
+It is possible that a single computation yields multiple annex keys, even when git-annex only asked for a single one (which is what it would do, sequentially, when driving a special remote). It would be good to be able to capture that and avoid needless duplication of computations.
+
+### Instruction deposition
+
+Using `STORE` (`git annex copy --to`) record instructions is possible (say particular ENV variables are used that pass information to a special remote), but is more or less a hack. It would be useful to have a dedicated command to accept and deposit such a record in association with one or more annex keys (which may or may not be known at that time). This likely require settling on a format for such records.
+
+### Storage redundancy tests
+
+I believe that no particular handling of annex key that are declared inputs to computing instructions for other keys are needed. Still listing it here to state that, and be possibly proven wrong.
+
+### Trust
+
+We would need a way for users to indicate that they trust a particular compute introduction or the entity that provided it. Even if git-annex does not implement tooling for that, it would be good to settle on a concept that can be interpreted/implemented by such special remotes.

comment
diff --git a/doc/todo/Setting_default_preferred_content_expressions/comment_3_971b3efe8a6ebdeede0184f13774e86b._comment b/doc/todo/Setting_default_preferred_content_expressions/comment_3_971b3efe8a6ebdeede0184f13774e86b._comment
new file mode 100644
index 0000000000..0594820a87
--- /dev/null
+++ b/doc/todo/Setting_default_preferred_content_expressions/comment_3_971b3efe8a6ebdeede0184f13774e86b._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 3"""
+ date="2024-04-10T16:58:39Z"
+ content="""
+I agree, after discussions at distribits it's clear there is use for this
+in datalad, and in git-annex generally.
+"""]]

todo
diff --git a/doc/todo/fsck_scrub.mdwn b/doc/todo/fsck_scrub.mdwn
new file mode 100644
index 0000000000..7f5f3c0934
--- /dev/null
+++ b/doc/todo/fsck_scrub.mdwn
@@ -0,0 +1,18 @@
+When a git-annex repository is on eg, a zfs or btrfs filesystem, the
+filesystem has built-in checksum verification of file. So a "scrub"
+operation that just reads all annexed files can detect when a file has
+gotten corrupt.
+
+This could be an enhancement to git-annex fsck, or a separate command.
+
+Note that this could detect corruption of files whose key does not
+contain a checksum.
+
+Since fsck reads the file content anyway when checksumming, 
+the enhancement could be an option to scrub files that don't use 
+checksums.
+
+It would make sense to move a file that is detected as corrupt to
+.git/annex/bad/ the same as fsck does.
+
+(Idea from Timothy Sanders.)

Added a comment
diff --git a/doc/forum/copying_annex_between_remotes_manually__63__/comment_3_875474a913d23823e3866cca27cfa3ac._comment b/doc/forum/copying_annex_between_remotes_manually__63__/comment_3_875474a913d23823e3866cca27cfa3ac._comment
new file mode 100644
index 0000000000..19fff278ac
--- /dev/null
+++ b/doc/forum/copying_annex_between_remotes_manually__63__/comment_3_875474a913d23823e3866cca27cfa3ac._comment
@@ -0,0 +1,16 @@
+[[!comment format=mdwn
+ username="unqueued"
+ avatar="http://cdn.libravatar.org/avatar/3bcbe0c9e9825637ad7efa70f458640d"
+ subject="comment 3"
+ date="2024-04-10T12:46:53Z"
+ content="""
+I would just clone the repo to the new machine, do `git annex init`, and then rsync the contents of `.git/annex/objects`, and then do `git annex fsck --all` to have to recheck every key it knows about.
+
+Alternatively, if you're concerned that there might be keys that weren't properly recorded somehow, in your new repo, after `.git/annex/objects` has been transferred, you can create an ingestion directory with a flat layout of the copied keys:
+
+```bash
+mkdir ingest && find .git/annex/objects -type f | xargs mv ingest && git annex reinject --known ingest/*
+```
+
+Finally, if you just want to rebuild it from scratch, do cp with the `-cL` option. If you are on macOS, it will make a reflink copy, and follow the symlinks. Delete the target .git dir and re-create it.
+"""]]

diff --git a/doc/forum/When_to_reuse_UUIDs_and_avoiding_UUID_clutter.mdwn b/doc/forum/When_to_reuse_UUIDs_and_avoiding_UUID_clutter.mdwn
new file mode 100644
index 0000000000..fc5b75ff15
--- /dev/null
+++ b/doc/forum/When_to_reuse_UUIDs_and_avoiding_UUID_clutter.mdwn
@@ -0,0 +1,15 @@
+I wanted to discuss cases for UUID reuse
+
+One reason is to mutate a special remote type. For example, a directory special remote to an rsync special remote and vice versa, passing along the uuid argument to initremote. Changing the directory layout is not hard. And you may wish to re-layout your .git/annex/objects directory to a different directory prefix and upload it to a cloud provider. If it supports an rclone remote that has hashing, you can verify it without having to redownload.
+
+Another good reason is to reuse a uuid is to avoid uuid namespace clutter.
+If you know ahead of time that you are storing data in repos that may later be merged, it makes sense to have a template annex repo to base a new repo off of, as well as store common settings and uuids.
+
+For example, I have a uuid space for multimedia annexes (peertube, youtube, podcasts, etc).
+
+The template comes preloaded with a uuid.log and remote.log. If my hostname is in the uuid.log, I reinit with that.
+
+If I must merge unrelated histories with conflicting name/uuid values, I first prefix my names with something. After a merge, I can do some gymnastics to make sure that the proper keys are set present for the respective uuid/name that I have chosen, and make the obsolete uuid/name dead. Simply making them dead is not enough, because even if a special remote uuid is marked dead, if the name is the same, it will still cause a conflict, so prefixing uuid/name collisions is importnat.
+
+I currently have a several annex template repos for different purposes (disk images, multimedia, etc). I have been meaning to automate this process more.
+

Added a comment
diff --git a/doc/forum/Using_git-annex_as_a_library/comment_10_cfded5c6325007c3f3f83818bd2e1dbc._comment b/doc/forum/Using_git-annex_as_a_library/comment_10_cfded5c6325007c3f3f83818bd2e1dbc._comment
new file mode 100644
index 0000000000..8583952330
--- /dev/null
+++ b/doc/forum/Using_git-annex_as_a_library/comment_10_cfded5c6325007c3f3f83818bd2e1dbc._comment
@@ -0,0 +1,16 @@
+[[!comment format=mdwn
+ username="unqueued"
+ avatar="http://cdn.libravatar.org/avatar/3bcbe0c9e9825637ad7efa70f458640d"
+ subject="comment 10"
+ date="2024-04-10T12:26:49Z"
+ content="""
+For my two cents, I have found git-annex to be a simple enough format that I have only needed basic helper scripts.
+
+But many operations can be done with one or a few lines of code.
+
+Git can do much of the heavy lifting for you in terms of looking stuff up from the git-annex branch, and I find the formats to be quite regular and easy to parse.
+
+I am thinking of bringing some of this together into a PHP library.
+
+But maybe I should just post my pure git-annex bash/perl one-liners.
+"""]]

diff --git a/doc/forum/Alternative_modes_for_annex_repos.mdwn b/doc/forum/Alternative_modes_for_annex_repos.mdwn
new file mode 100644
index 0000000000..6d707d3b14
--- /dev/null
+++ b/doc/forum/Alternative_modes_for_annex_repos.mdwn
@@ -0,0 +1,35 @@
+I have given a lot of thought and experimentation to how git-annex could be used for large projects where there is a desire to distribute files to many users, but where only a minority of users would would actually push key changes.
+
+The first option would be to have an annexless mode, where a local repo either has no uuid, or where the git-annex branch is not stored in the default namespace.
+
+This is for cases where the client only cares that a file exists in the repo and that it has been verified.
+
+One possibility could be `git-annex-get --no-init`, which would not init a local repo, but would get and verify a file. The existence of a file would simply be if the file exists. Only upon making a change can a could be fully inited.
+
+Or even better, in a restricted environment where git-annex is not available, this case is simple enough that getting a key from a url could be done with a shellscript. The url could be extracted from the upstream git-annex branch without checking it out, and the symlinks used for verification. However, there is a chance that the upstream git-annex branch may not be stable (like if it is not propagated after a mirror), so one could "shrinkwrap" keys and store their remote url locations in a .gitattributes file or somewhere else in the same branch. If key changes are desired, it can be fairly effortlessly upgraded to an actual git-annex repo.
+
+A step up from a completely annexless repo would be a hypothetical local-only git-annex repo, where git-annex only uses a git-annex branch locally.
+
+There could be a `git-annex-init --local` option which creates a `local/git-annex` branch, for local tracking, but would not sync to the server by default.
+
+In this mode, the upstream git-annex branch would just be pulled and kept read-only, and `local/git-annex` would keep local differences. The `local/git-annex` would just use the union driver to combine upstream changes with local changes. Upgrading to a full git-annex repo would be as easy as creating a new `git-annex` branch at the same commit id as `local/git-annex`
+
+
+So, in summary, I have considered two modes:
+
+Fully annex-less mode, which is simple enough to be implemented completely without git-annex, useful for restricted environments. Optionally, can use a kind of shinkwrapping to externalize key URLs to a file in the branch to guarantee that the fetch location is stored.
+
+Secondly is local mode, where a `local/git-annex` branch is downstream from a git-annex branch, and in order to sync changes back to the server, the repo is upgraded.
+
+Both of these modes could easily be upgraded to a full git-annex repo on demand.
+
+I think this is useful when considering large scale usage.
+
+
+Most of this functionality is something that is probably best suited for a wrapper.
+
+In terms of any any potential core changes to git-annex, it may be as simple as having a GIT_ANNEX_BRANCH environment variable, analogous to the GIT_DIR variable for git.
+
+Has anyone given any thought to scenarios like this?
+
+I think there are cases where developers use git-lfs and something like this might be a better fit. And also with making git-annex repos more generally available and portable.

Add link to Yann's distribits talk
diff --git a/doc/users/nobodyinperson.mdwn b/doc/users/nobodyinperson.mdwn
index 56a3d1ee05..b6eff5393c 100644
--- a/doc/users/nobodyinperson.mdwn
+++ b/doc/users/nobodyinperson.mdwn
@@ -8,6 +8,6 @@ I use git-annex to:
 
 I made a [Thunar plugin](https://gitlab.com/nobodyinperson/thunar-plugins) for git-annex, here's a [📹 screencast](https://fosstodon.org/@nobodyinperson/109836827575976439).
 
-In an attempt to [#gitAnnexAllTheThings](https://fosstodon.org/tags/gitAnnexAllTheThings), I used git annex as a backend for a cli time tracker [annextimelog](https://pypi.org/project/annextimelog/). It has similarities with timewarrior but adresses many of its inconveniences.
+In an attempt to [#gitAnnexAllTheThings](https://fosstodon.org/tags/gitAnnexAllTheThings), I used git annex as a backend for a cli time tracker [annextimelog](https://pypi.org/project/annextimelog/). It has similarities with timewarrior but adresses many of its inconveniences. I talked about it a bit at my [distribits 2024](https://distribits.live) talk, of which there is a recording [📹 on YouTube](https://www.youtube.com/watch?v=IdRUsn-zB2s).
 
 At the [Tübix 2023](https://www.tuebix.org/) I gave a git annex workshop, of which you can find a recording of the initial talk [📹 here (🇩🇪 German)](https://tube.tchncs.de/w/db1ec5ca-94ad-4f49-a507-2124fd699ff1) or [📹 here (English)](https://tube.tchncs.de/w/1U4vbTAhSEje3KQ1dGqvxh) in the fediverse and [📹 here on Odysee (🇩🇪 German)](https://odysee.com/@nobodyinperson:6/T%C3%BCbix2023-Yann-B%C3%BCchau-git-annex:6). 

multiple -m second try
Test suite passes this time. When committing the adjusted branch, use
the old method to make a message that old git-annex can consume. Also
made the code accept the new message, so that eventually
commitTreeExactMessage can be removed.
Sponsored-by: Kevin Mueller on Patreon
diff --git a/Annex/AdjustedBranch.hs b/Annex/AdjustedBranch.hs
index a7b6f03ed7..94fdeea2ea 100644
--- a/Annex/AdjustedBranch.hs
+++ b/Annex/AdjustedBranch.hs
@@ -468,19 +468,30 @@ commitAdjustedTree' treesha (BasisBranch basis) parents =
 			(commitAuthorMetaData basiscommit)
 			(commitCommitterMetaData basiscommit)
 			(mkcommit cmode)
-	mkcommit cmode = Git.Branch.commitTree cmode
+	-- Make sure that the exact message is used in the commit,
+	-- since that message is looked for later.
+	-- After git-annex 10.20240227, it's possible to use
+	-- commitTree instead of this, but this is being kept
+	-- for some time, for compatability with older versions.
+	mkcommit cmode = Git.Branch.commitTreeExactMessage cmode
 		adjustedBranchCommitMessage parents treesha
 
 {- This message should never be changed. -}
 adjustedBranchCommitMessage :: String
 adjustedBranchCommitMessage = "git-annex adjusted branch"
 
+{- Allow for a trailing newline after the message. -}
+hasAdjustedBranchCommitMessage :: Commit -> Bool
+hasAdjustedBranchCommitMessage c = 
+	dropWhileEnd (\x -> x == '\n' || x == '\r') (commitMessage c) 
+		== adjustedBranchCommitMessage
+
 findAdjustingCommit :: AdjBranch -> Annex (Maybe Commit)
 findAdjustingCommit (AdjBranch b) = go =<< catCommit b
   where
 	go Nothing = return Nothing
 	go (Just c)
-		| commitMessage c == adjustedBranchCommitMessage = return (Just c)
+		| hasAdjustedBranchCommitMessage c = return (Just c)
 		| otherwise = case commitParent c of
 			[p] -> go =<< catCommit p
 			_ -> return Nothing
@@ -540,7 +551,7 @@ propigateAdjustedCommits' warnwhendiverged origbranch adj _commitsprevented =
 		return (Right parent)
 	go origsha parent pastadjcommit (sha:l) = catCommit sha >>= \case
 		Just c
-			| commitMessage c == adjustedBranchCommitMessage ->
+			| hasAdjustedBranchCommitMessage c ->
 				go origsha parent True l
 			| pastadjcommit ->
 				reverseAdjustedCommit parent adj (sha, c) origbranch
@@ -577,7 +588,7 @@ reverseAdjustedCommit commitparent adj (csha, basiscommit) origbranch
 			(commitAuthorMetaData basiscommit)
 			(commitCommitterMetaData basiscommit) $
 				Git.Branch.commitTree cmode
-					(commitMessage basiscommit)
+					[commitMessage basiscommit]
 					[commitparent] treesha
 		return (Right revadjcommit)
 
diff --git a/Annex/AdjustedBranch/Merge.hs b/Annex/AdjustedBranch/Merge.hs
index 26ab0e7e3e..904f4ee412 100644
--- a/Annex/AdjustedBranch/Merge.hs
+++ b/Annex/AdjustedBranch/Merge.hs
@@ -153,7 +153,8 @@ mergeToAdjustedBranch tomerge (origbranch, adj) mergeconfig canresolvemerge comm
 			then do
 				cmode <- annexCommitMode <$> Annex.getGitConfig
 				c <- inRepo $ Git.Branch.commitTree cmode
-					("Merged " ++ fromRef tomerge) [adjmergecommit]
+					["Merged " ++ fromRef tomerge]
+					[adjmergecommit]
 					(commitTree currentcommit)
 				inRepo $ Git.Branch.update "updating adjusted branch" currbranch c
 				propigateAdjustedCommits origbranch adj
diff --git a/Annex/Branch.hs b/Annex/Branch.hs
index 9b5365b456..bcc9ae114d 100644
--- a/Annex/Branch.hs
+++ b/Annex/Branch.hs
@@ -945,9 +945,9 @@ rememberTreeishLocked treeish graftpoint jl = do
 	addedt <- inRepo $ Git.Tree.graftTree treeish graftpoint origtree
 	cmode <- annexCommitMode <$> Annex.getGitConfig
 	c <- inRepo $ Git.Branch.commitTree cmode
-		"graft" [branchref] addedt
+		["graft"] [branchref] addedt
 	c' <- inRepo $ Git.Branch.commitTree cmode
-		"graft cleanup" [c] origtree
+		["graft cleanup"] [c] origtree
 	inRepo $ Git.Branch.update' fullname c'
 	-- The tree in c' is the same as the tree in branchref,
 	-- and the index was updated to that above, so it's safe to
diff --git a/Annex/Import.hs b/Annex/Import.hs
index eaf41f4f79..2778740382 100644
--- a/Annex/Import.hs
+++ b/Annex/Import.hs
@@ -86,7 +86,7 @@ data ImportCommitConfig = ImportCommitConfig
 	{ importCommitTracking :: Maybe Sha
 	-- ^ Current commit on the remote tracking branch.
 	, importCommitMode :: Git.Branch.CommitMode
-	, importCommitMessage :: String
+	, importCommitMessages :: [String]
 	}
 
 {- Buils a commit for an import from a special remote.
@@ -251,7 +251,7 @@ buildImportCommit' remote importcommitconfig mtrackingcommit imported@(History t
 
 	mkcommit parents tree = inRepo $ Git.Branch.commitTree
 		(importCommitMode importcommitconfig)
-		(importCommitMessage importcommitconfig)
+		(importCommitMessages importcommitconfig)
 		parents
 		tree
 
diff --git a/Annex/RemoteTrackingBranch.hs b/Annex/RemoteTrackingBranch.hs
index ade303e02d..06591d0918 100644
--- a/Annex/RemoteTrackingBranch.hs
+++ b/Annex/RemoteTrackingBranch.hs
@@ -77,7 +77,7 @@ makeRemoteTrackingBranchMergeCommit' commitsha importedhistory treesha = do
 	cmode <- annexCommitMode <$> Annex.getGitConfig
 	inRepo $ Git.Branch.commitTree
 			cmode
-			"remote tracking branch"
+			["remote tracking branch"]
 			[commitsha, importedhistory]
 			treesha
 
diff --git a/Annex/View.hs b/Annex/View.hs
index b47e34564b..7372287380 100644
--- a/Annex/View.hs
+++ b/Annex/View.hs
@@ -577,7 +577,7 @@ updateView view madj = do
 			cmode <- annexCommitMode <$> Annex.getGitConfig
 			let msg = "updated " ++ fromRef (branchView view madj)
 			let parent = catMaybes [oldcommit]
-			inRepo (Git.Branch.commitTree cmode msg parent newtree)
+			inRepo (Git.Branch.commitTree cmode [msg] parent newtree)
 		else return Nothing
 
 {- Diff between currently checked out branch and staged changes, and
diff --git a/CHANGELOG b/CHANGELOG
index 83dfa02de9..3a4adca108 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -13,6 +13,8 @@ git-annex (10.20240228) UNRELEASED; urgency=medium
     the same repository.
   * Windows: Fix escaping output to terminal when using old
     versions of MinTTY.
+  * sync, assist, import: Allow -m option to be specified multiple
+    times, to provide additional paragraphs for the commit message.
 
  -- Joey Hess <id@joeyh.name>  Tue, 27 Feb 2024 13:07:10 -0400
 
diff --git a/Command/FilterBranch.hs b/Command/FilterBranch.hs
index 10f03cccda..6c565c5d29 100644
--- a/Command/FilterBranch.hs
+++ b/Command/FilterBranch.hs
@@ -189,7 +189,7 @@ seek o = withOtherTmp $ \tmpdir -> do
 	liftIO $ removeWhenExistsWith removeLink tmpindex
 	cmode <- annexCommitMode <$> Annex.getGitConfig
 	cmessage <- Annex.Branch.commitMessage
-	c <- inRepo $ Git.commitTree cmode cmessage [] t
+	c <- inRepo $ Git.commitTree cmode [cmessage] [] t
 	liftIO $ putStrLn (fromRef c)
   where
 	ww = WarnUnmatchLsFiles "filter-branch"
diff --git a/Command/Import.hs b/Command/Import.hs
index a37064eefc..f5483cc7d5 100644
--- a/Command/Import.hs
+++ b/Command/Import.hs
@@ -1,6 +1,6 @@
 {- git-annex command
  -
- - Copyright 2012-2021 Joey Hess <id@joeyh.name>
+ - Copyright 2012-2024 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -70,7 +70,7 @@ data ImportOptions
 		, importToSubDir :: Maybe FilePath
 		, importContent :: Bool
 		, checkGitIgnoreOption :: CheckGitIgnore
-		, messageOption :: Maybe String
+		, messageOption :: [String]
 		}
 
 optParser :: CmdParamsDesc -> Parser ImportOptions
@@ -82,7 +82,7 @@ optParser desc = do
 		)
 	dupmode <- fromMaybe Default <$> optional duplicateModeParser
 	ic <- Command.Add.checkGitIgnoreSwitch
-	message <- optional (strOption
+	message <- many (strOption
 		( long "message" <> short 'm' <> metavar "MSG"
 		<> help "commit message"
 		))
@@ -322,8 +322,8 @@ verifyExisting key destfile (yes, no) = do
 	verifyEnoughCopiesToDrop [] key Nothing needcopies mincopies [] preverified tocheck
 		(const yes) no
 
-seekRemote :: Remote -> Branch -> Maybe TopFilePath -> Bool -> CheckGitIgnore -> Maybe String -> CommandSeek
-seekRemote remote branch msubdir importcontent ci mimportmessage = do
+seekRemote :: Remote -> Branch -> Maybe TopFilePath -> Bool -> CheckGitIgnore -> [String] -> CommandSeek
+seekRemote remote branch msubdir importcontent ci importmessages = do
 	importtreeconfig <- case msubdir of
 		Nothing -> return ImportTree
 		Just subdir ->

(Diff truncated)
Added a comment: Update after transferring more files
diff --git a/doc/bugs/available_space_miscomputed_on_large_macOS_volume/comment_1_cb1e92ef1412e68c12f6a1c9a10a7414._comment b/doc/bugs/available_space_miscomputed_on_large_macOS_volume/comment_1_cb1e92ef1412e68c12f6a1c9a10a7414._comment
new file mode 100644
index 0000000000..ea323e9c51
--- /dev/null
+++ b/doc/bugs/available_space_miscomputed_on_large_macOS_volume/comment_1_cb1e92ef1412e68c12f6a1c9a10a7414._comment
@@ -0,0 +1,16 @@
+[[!comment format=mdwn
+ username="mdekstrand"
+ avatar="http://cdn.libravatar.org/avatar/0acb8a6c848d39aa53d94bd81239b034"
+ subject="Update after transferring more files"
+ date="2024-04-09T14:45:07Z"
+ content="""
+After transferring many more files with `--force`, `git annex info` reports an available space of 17.46 terabytes, with following actual space:
+
+```
+$ df -h .
+Filesystem      Size    Used   Avail Capacity iused ifree %iused  Mounted on
+/dev/disk5s1    98Ti   2.3Ti    96Ti     3%     75k  1.0T    0%   /Volumes/ABYSS
+```
+
+I suspect the problem is either an integer overflow of some kind, or an integer size/precision error reading from the disk space struct in the disk space library.
+"""]]

Added a comment: or use numcopies for safety
diff --git a/doc/todo/Idea_for_emulating_a_versioned_tree_export/comment_3_19e77a51911d3c2d39a558162223e78e._comment b/doc/todo/Idea_for_emulating_a_versioned_tree_export/comment_3_19e77a51911d3c2d39a558162223e78e._comment
new file mode 100644
index 0000000000..2968adabb7
--- /dev/null
+++ b/doc/todo/Idea_for_emulating_a_versioned_tree_export/comment_3_19e77a51911d3c2d39a558162223e78e._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="nobodyinperson"
+ avatar="http://cdn.libravatar.org/avatar/736a41cd4988ede057bae805d000f4f5"
+ subject="or use numcopies for safety"
+ date="2024-04-09T10:48:10Z"
+ content="""
+Or, even better: If such an export action would cause data loss, the potentially vanishing keys would be known, because they reside in a git-tree that you diff anyway, right? Git annex could then apply its usual logic of 'I shall never execute an action that reduces a file's copies below `$numcopies`' and error out with an explanation. That feels right and consistent with git annex' typical behavior.
+"""]]

Added a comment: when someone names files like keys, they probably want trouble 🙃
diff --git a/doc/todo/Idea_for_emulating_a_versioned_tree_export/comment_2_e0f6d78620a89f60010607b37328f754._comment b/doc/todo/Idea_for_emulating_a_versioned_tree_export/comment_2_e0f6d78620a89f60010607b37328f754._comment
new file mode 100644
index 0000000000..1fd0ecfa3b
--- /dev/null
+++ b/doc/todo/Idea_for_emulating_a_versioned_tree_export/comment_2_e0f6d78620a89f60010607b37328f754._comment
@@ -0,0 +1,14 @@
+[[!comment format=mdwn
+ username="nobodyinperson"
+ avatar="http://cdn.libravatar.org/avatar/736a41cd4988ede057bae805d000f4f5"
+ subject="when someone names files like keys, they probably want trouble 🙃"
+ date="2024-04-09T10:43:16Z"
+ content="""
+> skip exporting any files that have names that look like annex keys.
+
+A general regex for a key looks like `^[A-Z0-9]+(?:-[a-z]\d+)*--.+$`, right? This seems like there would be many possible false positives that would not be exported, like `GIT--The Book.pdf`. 
+
+I can't see a situation where git annex would produce or rename files named like their (or another) key. So if someone deliberately names a file like `SHA256E-s31390--f50d7ac4c6b9031379986bc362fcefb65f1e52621ce1708d537e740fefc59cc0.mp3` and then exports it to such a versioned tree, I'd be fine with overwriting. After all, export remotes aren't primarily for full backups, rather for access convenience and filesystem limitations, right?
+
+I don't know if it's a problem internally for git-annex though when an export suddenly causes keys to vanish from the remote, but I guess your git-tree-diffing automatically takes care of that?
+"""]]

comment
diff --git a/doc/todo/Idea_for_emulating_a_versioned_tree_export/comment_1_d944fd707130be6ebe35302794a0b73f._comment b/doc/todo/Idea_for_emulating_a_versioned_tree_export/comment_1_d944fd707130be6ebe35302794a0b73f._comment
new file mode 100644
index 0000000000..dba76ec6b8
--- /dev/null
+++ b/doc/todo/Idea_for_emulating_a_versioned_tree_export/comment_1_d944fd707130be6ebe35302794a0b73f._comment
@@ -0,0 +1,24 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 1"""
+ date="2024-04-08T20:46:26Z"
+ content="""
+When we were talking about this idea, I thought there was a problem, but
+didn't quite manage to find it then. 
+
+I see it now: If `foo` is an annexed file that gets exported
+this way to `foo/SHA--x`, and then that annexed file
+is deleted and a new annexed file `foo/SHA--x` is added,
+it will want to export it to `foo/SHA--x/SHA--y`.
+
+It would either fail because the file exists, or delete it and replace
+it with the directory. The former would cause the export to fail, the
+latter could case data loss. It's not defined what a special remote will do
+in this situation.
+
+It seems that this case would never occur accidentially, but it's still worth
+considering it.
+
+Perhaps it should simply skip exporting any files that have names
+that look like annex keys.
+"""]]

repot MacOS disk usage bug
diff --git a/doc/bugs/available_space_miscomputed_on_large_macOS_volume.mdwn b/doc/bugs/available_space_miscomputed_on_large_macOS_volume.mdwn
new file mode 100644
index 0000000000..82a882df62
--- /dev/null
+++ b/doc/bugs/available_space_miscomputed_on_large_macOS_volume.mdwn
@@ -0,0 +1,78 @@
+### Please describe the problem.
+
+`git-annex` is misreporting the available disk space on my large (98TiB) APFS volume on MacOS.
+
+### What steps will reproduce the problem?
+
+- acquire a large storage array (how large, unclear)
+- format with APFS on MacOS (no idea if it also occurs on on other platforms or file systems)
+- clone a git-annex repo onto it
+- look at `git annex info` / try to copy data
+
+Here's my `df` output:
+
+```
+Filesystem      Size    Used   Avail Capacity iused ifree %iused  Mounted on
+/dev/disk5s1    98Ti   2.2Ti    96Ti     3%     54k  1.0T    0%   /Volumes/ABYSS
+```
+
+And here's the `git annex info`:
+
+```
+trusted repositories: 0
+semitrusted repositories: 8
+	00000000-0000-0000-0000-000000000001 -- web
+ 	00000000-0000-0000-0000-000000000002 -- bittorrent
+ 	335d1827-1f6e-40d8-ae70-23ba9dd4b4a6 -- borg
+ 	4d887674-fb3f-47ee-afea-487f0143950e -- dvc
+ 	512ca1ff-43b7-4537-bf64-0f55e1ba2e8a -- cci server [origin]
+ 	71414f81-c837-4838-894d-840d0a2170ff -- [pocket]
+ 	dd695a7f-4b41-4a7d-ae57-3174f6a839e1 -- bsu server [grue]
+ 	df875036-d197-46f8-940c-9df5dc7fc2cd -- abyss [here]
+untrusted repositories: 0
+transfers in progress: none
+available local disk space: 3.94 gigabytes (+100 megabytes reserved)
+local annex keys: 14380
+local annex size: 4.18 terabytes
+annexed files in working tree: 13312
+size of annexed files in working tree: 2.59 terabytes
+combined annex size of all repositories: 19.09 terabytes (+ 9 unknown size)
+annex sizes of repositories:
+	  5.57 TB: 512ca1ff-43b7-4537-bf64-0f55e1ba2e8a -- cci server [origin]
+ 	  4.18 TB: df875036-d197-46f8-940c-9df5dc7fc2cd -- abyss [here]
+ 	  3.21 TB: 71414f81-c837-4838-894d-840d0a2170ff -- [pocket]
+ 	  2.52 TB: 335d1827-1f6e-40d8-ae70-23ba9dd4b4a6 -- borg
+ 	  1.92 TB: dd695a7f-4b41-4a7d-ae57-3174f6a839e1 -- bsu server [grue]
+ 	879.87 GB: 00000000-0000-0000-0000-000000000001 -- web
+ 	819.55 GB: 4d887674-fb3f-47ee-afea-487f0143950e -- dvc
+backend usage:
+	SHA256: 6991
+	SHA256E: 6321
+bloom filter size: 32 mebibytes (2.9% full)
+```
+
+### What version of git-annex are you using? On what operating system?
+
+MacOS Sonoma 14.4.1, Git Annex version below:
+
+```
+git-annex version: 10.20240227
+build flags: Assistant Webapp Pairing FsEvents TorrentParser MagicMime Benchmark Feeds Testsuite S3 WebDAV
+dependency versions: aws-0.24.1 bloomfilter-2.0.1.2 crypton-0.34 DAV-1.3.4 feed-1.3.2.1 ghc-9.6.3 http-client-0.7.16 persistent-sqlite-2.13.3.0 torrent-10000.1.3 uuid-1.3.15 yesod-1.6.2.1
+key/value backends: SHA256E SHA256 SHA512E SHA512 SHA224E SHA224 SHA384E SHA384 SHA3_256E SHA3_256 SHA3_512E SHA3_512 SHA3_224E SHA3_224 SHA3_384E SHA3_384 SKEIN256E SKEIN256 SKEIN512E SKEIN512 BLAKE2B256E BLAKE2B256 BLAKE2B512E BLAKE2B512 BLAKE2B160E BLAKE2B160 BLAKE2B224E BLAKE2B224 BLAKE2B384E BLAKE2B384 BLAKE2BP512E BLAKE2BP512 BLAKE2S256E BLAKE2S256 BLAKE2S160E BLAKE2S160 BLAKE2S224E BLAKE2S224 BLAKE2SP256E BLAKE2SP256 BLAKE2SP224E BLAKE2SP224 SHA1E SHA1 MD5E MD5 WORM URL X*
+remote types: git gcrypt p2p S3 bup directory rsync web bittorrent webdav adb tahoe glacier ddar git-lfs httpalso borg hook external
+operating system: darwin aarch64
+supported repository versions: 8 9 10
+upgrade supported from repository versions: 0 1 2 3 4 5 6 7 8 9 10
+local repository version: 10
+```
+
+### Please provide any additional information below.
+
+I don't know what other file system versions (or operating systems) this may appear in, as this is the only 98TiB volume I have right now.
+
+I'm happy to do some poking around or test proposed fixes, I have some Haskell experience but it's been a while. My current instinct is that either `disk-free-space` is misbehaving on a disk this large for unclear reasons.
+
+### Have you had any luck using git-annex before? (Sometimes we get tired of reading bug reports all day and a lil' positive end note does wonders)
+
+Been successfully using it for the last year+, for managing archives of academic research & teaching work and the assets for my web site. Generally quite happy :). Like it enough I want to use it schlep a few terabytes (and growing) onto my new Thunderbolt RAID array...

diff --git a/doc/todo/Idea_for_emulating_a_versioned_tree_export.mdwn b/doc/todo/Idea_for_emulating_a_versioned_tree_export.mdwn
index 21ac9ad683..18695f80db 100644
--- a/doc/todo/Idea_for_emulating_a_versioned_tree_export.mdwn
+++ b/doc/todo/Idea_for_emulating_a_versioned_tree_export.mdwn
@@ -3,6 +3,7 @@ Dear Joey,
 During DistriBits 2024, we discussed a concept that you seemed to like: emulating versioned tree export on a special remote with a non-versioned filesystem. This could be a generic mechanism of git-annex. Maybe a new option for the special remote (say: 'versioning = yes / no / emulated' or 'exporttree = yes / no / emulated')?
 
 The idea is to save target files in the remote at paths reflecting the ones in the repo, but:
+
 * create an extra directory at the end of the path identical to the filename,
 * directory name includes the original extension of the file, which may seem a bit odd, but ensures no ambiguities,
 * inside the directory, save the file under filename = key (preferably add the original extension).
@@ -52,6 +53,7 @@ Example: the content of the git-annex repo and remote filesystem after a few tre
 ----------------------
 
 Advantages:
+
 * easy to implement,
 * you get (kind of) versioning on any POSIX-like filesystem,
 * older versions of files are never overwritten (history tracking),
@@ -59,6 +61,7 @@ Advantages:
 * users can use the remote filesystem directly, as it represents something meaningful.
 
 Disadvantages:
+
 * not perfect,
 * users need to accept the inconvenience caused by file naming on the bottom level,
 * it may be hard to find the right file version in the remote, especially if there are lots of them;

diff --git a/doc/todo/Idea_for_emulating_a_versioned_tree_export.mdwn b/doc/todo/Idea_for_emulating_a_versioned_tree_export.mdwn
index a38988b0f6..21ac9ad683 100644
--- a/doc/todo/Idea_for_emulating_a_versioned_tree_export.mdwn
+++ b/doc/todo/Idea_for_emulating_a_versioned_tree_export.mdwn
@@ -3,9 +3,9 @@ Dear Joey,
 During DistriBits 2024, we discussed a concept that you seemed to like: emulating versioned tree export on a special remote with a non-versioned filesystem. This could be a generic mechanism of git-annex. Maybe a new option for the special remote (say: 'versioning = yes / no / emulated' or 'exporttree = yes / no / emulated')?
 
 The idea is to save target files in the remote at paths reflecting the ones in the repo, but:
-- create an extra directory at the end of the path identical to the filename,
-- directory name includes the original extension of the file, which may seem a bit odd, but ensures no ambiguities,
-- inside the directory, save the file under filename = key (preferably add the original extension).
+* create an extra directory at the end of the path identical to the filename,
+* directory name includes the original extension of the file, which may seem a bit odd, but ensures no ambiguities,
+* inside the directory, save the file under filename = key (preferably add the original extension).
 
 Example: the content of the git-annex repo and remote filesystem after a few tree exports:
 
@@ -52,16 +52,16 @@ Example: the content of the git-annex repo and remote filesystem after a few tre
 ----------------------
 
 Advantages:
-- easy to implement,
-- you get (kind of) versioning on any POSIX-like filesystem,
-- older versions of files are never overwritten (history tracking),
-- it's sufficient to push only the changed files,
-- users can use the remote filesystem directly, as it represents something meaningful.
+* easy to implement,
+* you get (kind of) versioning on any POSIX-like filesystem,
+* older versions of files are never overwritten (history tracking),
+* it's sufficient to push only the changed files,
+* users can use the remote filesystem directly, as it represents something meaningful.
 
 Disadvantages:
-- not perfect,
-- users need to accept the inconvenience caused by file naming on the bottom level,
-- it may be hard to find the right file version in the remote, especially if there are lots of them;
+* not perfect,
+* users need to accept the inconvenience caused by file naming on the bottom level,
+* it may be hard to find the right file version in the remote, especially if there are lots of them;
 	- modification times will certainly help here,
 	- can we concatenate some extra information to the file names that could help in identification?
 

diff --git a/doc/todo/Idea_for_emulating_a_versioned_tree_export.mdwn b/doc/todo/Idea_for_emulating_a_versioned_tree_export.mdwn
new file mode 100644
index 0000000000..a38988b0f6
--- /dev/null
+++ b/doc/todo/Idea_for_emulating_a_versioned_tree_export.mdwn
@@ -0,0 +1,69 @@
+Dear Joey,
+
+During DistriBits 2024, we discussed a concept that you seemed to like: emulating versioned tree export on a special remote with a non-versioned filesystem. This could be a generic mechanism of git-annex. Maybe a new option for the special remote (say: 'versioning = yes / no / emulated' or 'exporttree = yes / no / emulated')?
+
+The idea is to save target files in the remote at paths reflecting the ones in the repo, but:
+- create an extra directory at the end of the path identical to the filename,
+- directory name includes the original extension of the file, which may seem a bit odd, but ensures no ambiguities,
+- inside the directory, save the file under filename = key (preferably add the original extension).
+
+Example: the content of the git-annex repo and remote filesystem after a few tree exports:
+
+	[git-annex]
+	repo-name
+	|
+	|-- phd
+	|   |
+	|   |-- thesis.pdf
+	|   |-- images
+	|       |
+	|       |-- figure.png
+	|       |-- diagram.png
+	|
+	|-- guidelines.pdf
+
+
+	[remote filesystem]
+	repo-name
+	|
+	|-- phd
+	|   |
+	|   |-- thesis.pdf
+	|   |   |
+	|   |   |--- SHA256E-...-key1.pdf
+	|   |   |--- SHA256E-...-key2.pdf
+	|   |   |--- SHA256E-...-key3.pdf
+	|   | 
+	|   |-- images
+	|       |
+	|       |-- figure.png
+	|       |   |
+	|       |   |-- SHA256E-...-key1.png
+	|       |   
+	|       |-- diagram.png
+	|           |
+	|           |-- SHA256E-...-key1.png
+	|           |-- SHA256E-...-key2.png
+	|
+	|-- guidelines.pdf
+	|   |
+	|   |-- SHA256E-...-key1.pdf
+
+----------------------
+
+Advantages:
+- easy to implement,
+- you get (kind of) versioning on any POSIX-like filesystem,
+- older versions of files are never overwritten (history tracking),
+- it's sufficient to push only the changed files,
+- users can use the remote filesystem directly, as it represents something meaningful.
+
+Disadvantages:
+- not perfect,
+- users need to accept the inconvenience caused by file naming on the bottom level,
+- it may be hard to find the right file version in the remote, especially if there are lots of them;
+	- modification times will certainly help here,
+	- can we concatenate some extra information to the file names that could help in identification?
+
+
+Feel free to contact me, I'd be happy to discuss and help make this happen.

update
diff --git a/doc/todo/git-remote-annex.mdwn b/doc/todo/git-remote-annex.mdwn
index 7109738d0f..5693c40da1 100644
--- a/doc/todo/git-remote-annex.mdwn
+++ b/doc/todo/git-remote-annex.mdwn
@@ -6,7 +6,8 @@ It will be a safer implementation, will support incremental pushes, and
 will be available to users who don't use datalad. 
 
 Work is in the `git-remote-annex` branch, currently we have a design for
-the core data files. 
+the core data files and operations.
+<http://source.git-annex.branchable.com/?p=source.git;a=blob;f=doc/internals/git-remote-annex.mdwn;hb=git-remote-annex>
 
 I still need to do some design work around using the git-annex branch to
 detect concurrent push situations where changes to the manifest get lost,

diff --git a/doc/todo/absolute_symlinks.mdwn b/doc/todo/absolute_symlinks.mdwn
new file mode 100644
index 0000000000..348ef67b8c
--- /dev/null
+++ b/doc/todo/absolute_symlinks.mdwn
@@ -0,0 +1,7 @@
+A lot of my git-annex content doesn't have a "stable" location within the repository, I might decide to move things to subdirectories or out of subdirectories. This always invalidates the relative `../../.git/` symlinks that git-annex uses and I have to `annex fix` them every time I move things around. This is especially useful when I'm accessing my homeserver over SMB from Windows PCs, where symlinks are followed automatically – and broken symlinks simply don't show up via SMB at all, so moving a file into a subfolder causes it to "disappear" until `annex fix`.
+
+But the repository as a whole does have a stable location in the filesystem, so it would be much more convenient if I had an option for the item symlinks to use absolute paths, e.g. `Foo.mkv -> /hdd/Videos/.git/annex/[etc]`. (I *could* ensure that the `/hdd/Videos` path is valid on all the hosts that the repository is on, and I don't mind running `annex fix` on the rare occasion when I need to move it.)
+
+I thought such an option already exists, but could not find it anywhere within the git-annex manual.
+
+I considered having the files unlocked (which seems to work well with Btrfs automatically providing thin copies even without needing to use the fragile-looking annex.thin mode), but 1) even missing files appear as regular files when they're unlocked, which is *very* confusing; 2) the files are no longer read-only and I don't like the idea of some tool accidentally damaging them.

project we started at Distribits
diff --git a/doc/todo/git-remote-annex.mdwn b/doc/todo/git-remote-annex.mdwn
new file mode 100644
index 0000000000..7109738d0f
--- /dev/null
+++ b/doc/todo/git-remote-annex.mdwn
@@ -0,0 +1,19 @@
+git-remote-annex will be a program that allows push/pull of a git
+repository to any git-annex special remote.
+
+This is a redesign and reimplementation of git-remote-datalad-annex. 
+It will be a safer implementation, will support incremental pushes, and
+will be available to users who don't use datalad. 
+
+Work is in the `git-remote-annex` branch, currently we have a design for
+the core data files. 
+
+I still need to do some design work around using the git-annex branch to
+detect concurrent push situations where changes to the manifest get lost,
+and re-add those changes to it later.
+
+Also, it's not clear what will happen when two people make conflicting pushes
+to a ref, the goal would be to replicate git push to a regular git remote,
+but that may not be entirely possible. This will need to be investigated
+further.
+--[[Joey]]

Added a comment: more use cases for configurable default preferred content
diff --git a/doc/todo/Setting_default_preferred_content_expressions/comment_2_575320e07f005d46d26cc6db9ad3ebd3._comment b/doc/todo/Setting_default_preferred_content_expressions/comment_2_575320e07f005d46d26cc6db9ad3ebd3._comment
new file mode 100644
index 0000000000..9ebfd99b8e
--- /dev/null
+++ b/doc/todo/Setting_default_preferred_content_expressions/comment_2_575320e07f005d46d26cc6db9ad3ebd3._comment
@@ -0,0 +1,12 @@
+[[!comment format=mdwn
+ username="nobodyinperson"
+ avatar="http://cdn.libravatar.org/avatar/736a41cd4988ede057bae805d000f4f5"
+ subject="more use cases for configurable default preferred content"
+ date="2024-04-06T15:12:50Z"
+ content="""
+This came up again at the distribits meeting.
+
+DataLad itself is designed to work like `git annex wanted . present` (i.e. content is supposed to be fetched manually. It is assumed that the user does not generally want all content of a DataLad dataset / git annex repo). DataLad could itself run `git annex wanted . present` as part of its setup (talked about that with @mih), but I still think a setting in the git-annex branch that auto-sets the above settings in fresh clones (even when using plain git annex, not DataLad), is useful. It enhances the user experience of sparse checkouts (a `git annex assist` in a freshly cloned annex repo can then be configured to only pull specific or no files).
+
+I also discussed it with people in the context of handling confidential patient data that should not necessarily be copied everywhere. The default of just wanting all worktree content increases the delicacy of the matter a bit. Were there a way to have fresh clones (or even freshly created remotes that were not yet given a preferred content manually) have a preconfigured default wanted content, it would reduce the possibility of confidential data accidentally being copied all over the place.
+"""]]

Added a comment
diff --git a/doc/bugs/migrate_removes_associated_URLs_with_custom_scheme/comment_1_6e9a2b3ff2be4e2b3ff5b67ac913efb8._comment b/doc/bugs/migrate_removes_associated_URLs_with_custom_scheme/comment_1_6e9a2b3ff2be4e2b3ff5b67ac913efb8._comment
new file mode 100644
index 0000000000..5f493c6841
--- /dev/null
+++ b/doc/bugs/migrate_removes_associated_URLs_with_custom_scheme/comment_1_6e9a2b3ff2be4e2b3ff5b67ac913efb8._comment
@@ -0,0 +1,9 @@
+[[!comment format=mdwn
+ username="m.risse@77eac2c22d673d5f10305c0bade738ad74055f92"
+ nickname="m.risse"
+ avatar="http://cdn.libravatar.org/avatar/59541f50d845e5f81aff06e88a38b9de"
+ subject="comment 1"
+ date="2024-04-06T10:05:04Z"
+ content="""
+Looking at this again I am also surprised that I didn't need to set the equivalent of `git config --local remote.cds.annex-security-allow-unverified-downloads ACKTHPPT` for the web special remote when get'ing a file with just a URL key.
+"""]]

diff --git a/doc/bugs/migrate_removes_associated_URLs_with_custom_scheme.mdwn b/doc/bugs/migrate_removes_associated_URLs_with_custom_scheme.mdwn
new file mode 100644
index 0000000000..7ebff66146
--- /dev/null
+++ b/doc/bugs/migrate_removes_associated_URLs_with_custom_scheme.mdwn
@@ -0,0 +1,142 @@
+### Please describe the problem.
+
+With URLs that are handled by the web remote the URL will be kept through a migration, i.e.
+
+```
+git annex addurl --relaxed <some-http-url>
+git annex get <the-added-file>
+git annex migrate
+```
+
+will migrate the key of the file to be hash based, and keep the URL associated to that key.
+If I do the same with a URL whose scheme is handled by a custom special remote (this one specifically is what I got the issue with: <https://github.com/matrss/datalad-cds/blob/main/src/datalad_cds/cds_remote.py>, it registers itself for the `cds:` scheme), the URL seems to be dropped from the key (i.e. whereis no longer shows it and git annex can no longer fetch it from the special remote).
+
+### What steps will reproduce the problem?
+
+The steps mentioned above for a URL whose scheme is handled by an (external?) special remote. Specifically, I saw it with datalad-cds, it might happen for other special remotes as well.
+
+### What version of git-annex are you using? On what operating system?
+
+```
+git-annex version: 10.20231129
+build flags: Assistant Webapp Pairing Inotify DBus DesktopNotify TorrentParser MagicMime Feeds Testsuite S3 WebDAV
+dependency versions: aws-0.24.1 bloomfilter-2.0.1.2 crypton-0.32 DAV-1.3.4 feed-1.3.2.1 ghc-9.4.8 http-client-0.7.15 persistent-sqlite-2.13.3.0 torrent-10000.1.3 uuid-1.3.15 yesod-1.6.2.1
+key/value backends: SHA256E SHA256 SHA512E SHA512 SHA224E SHA224 SHA384E SHA384 SHA3_256E SHA3_256 SHA3_512E SHA3_512 SHA3_224E SHA3_224 SHA3_384E SHA3_384 SKEIN256E SKEIN256 SKEIN512E SKEIN512 BLAKE2B256E BLAKE2B256 BLAKE2B512E BLAKE2B512 BLAKE2B160E BLAKE2B160 BLAKE2B224E BLAKE2B224 BLAKE2B384E BLAKE2B384 BLAKE2BP512E BLAKE2BP512 BLAKE2S256E BLAKE2S256 BLAKE2S160E BLAKE2S160 BLAKE2S224E BLAKE2S224 BLAKE2SP256E BLAKE2SP256 BLAKE2SP224E BLAKE2SP224 SHA1E SHA1 MD5E MD5 WORM URL X*
+remote types: git gcrypt p2p S3 bup directory rsync web bittorrent webdav adb tahoe glacier ddar git-lfs httpalso borg hook external
+operating system: linux x86_64
+supported repository versions: 8 9 10
+upgrade supported from repository versions: 0 1 2 3 4 5 6 7 8 9 10
+local repository version: 10
+```
+
+Installed with nix from nixpkgs on an ubuntu system.
+
+### Please provide any additional information below.
+
+It works with the web remote:
+[[!format sh """
+# If you can, paste a complete transcript of the problem occurring here.
+# If the problem is with the git-annex assistant, paste in .git/annex/daemon.log
+$ datalad create test-ds
+create(ok): [...] (dataset)
+$ cd test-ds/
+$ git annex addurl --relaxed "https://git-annex.branchable.com/"
+addurl https://git-annex.branchable.com/ (to git-annex.branchable.com_) ok
+(recording state in git...)
+$ git annex whereis git-annex.branchable.com_ 
+whereis git-annex.branchable.com_ (1 copy) 
+  	00000000-0000-0000-0000-000000000001 -- web
+
+  web: https://git-annex.branchable.com/
+ok
+$ ls -l
+[...] git-annex.branchable.com_ -> '.git/annex/objects/pJ/v4/URL--https&c%%git-annex.branchable.com%/URL--https&c%%git-annex.branchable.com%'
+$ git annex get git-annex.branchable.com_ 
+get git-annex.branchable.com_ (from web...) 
+ok
+(recording state in git...)
+$ git annex migrate
+migrate git-annex.branchable.com_ (checksum...) ok
+(recording state in git...)
+$ ls -l
+[...] git-annex.branchable.com_ -> .git/annex/objects/31/qv/MD5E-s12462--6b956d66f8352205df79936ada326ec3/MD5E-s12462--6b956d66f8352205df79936ada326ec3
+$ git annex whereis git-annex.branchable.com_ 
+whereis git-annex.branchable.com_ (2 copies) 
+  	00000000-0000-0000-0000-000000000001 -- web
+   	60079e0e-42e4-492e-a7b1-dde764d069eb -- [here]
+
+  web: https://git-annex.branchable.com/
+ok
+# End of transcript or log.
+"""]]
+
+It doesn't work with the custom special remote:
+[[!format sh """
+$ datalad create test-ds
+create(ok): [...] (dataset)
+$ cd test-ds/
+$ datalad download-cds --lazy --nosave --path "2022-01-01.grib" "$(cat <<EOF
+{
+    "dataset": "reanalysis-era5-complete",
+    "sub-selection": {
+        "class": "ea",
+        "date": "2022-01-01",
+        "expver": "1",
+        "levelist": "1",
+        "levtype": "ml",
+        "param": "130",
+        "stream": "oper",
+        "time": "00:00:00/06:00:00/12:00:00/18:00:00",
+        "type": "an",                                 
+        "grid": ".3/.3"
+    }
+}
+EOF
+)"
+cds(ok): [...] (dataset)
+$ ls -l
+[...] 2022-01-01.grib -> '.git/annex/objects/Mx/4G/URL--cds&cv1-eyJkYXRhc2V0IjoicmVhbmFs-162a71d794c333f5e04b13283421a49a/URL--cds&cv1-eyJkYXRhc2V0IjoicmVhbmFs-162a71d794c333f5e04b13283421a49a'
+$ git annex whereis 2022-01-01.grib 
+whereis 2022-01-01.grib (1 copy) 
+  	923e2755-e747-42f4-890a-9c921068fb82 -- [cds]
+
+  cds: {"dataset":"reanalysis-era5-complete","sub-selection":{"class":"ea","date":"2022-01-01","expver":"1","levelist":"1","levtype":"ml","param":"130","stream":"oper","time":"00:00:00/06:00:00/12:00:00/18:00:00","type":"an","grid":".3/.3"}}
+  cds: cds:v1-eyJkYXRhc2V0IjoicmVhbmFseXNpcy1lcmE1LWNvbXBsZXRlIiwic3ViLXNlbGVjdGlvbiI6eyJjbGFzcyI6ImVhIiwiZGF0ZSI6IjIwMjItMDEtMDEiLCJleHB2ZXIiOiIxIiwibGV2ZWxpc3QiOiIxIiwibGV2dHlwZSI6Im1sIiwicGFyYW0iOiIxMzAiLCJzdHJlYW0iOiJvcGVyIiwidGltZSI6IjAwOjAwOjAwLzA2OjAwOjAwLzEyOjAwOjAwLzE4OjAwOjAwIiwidHlwZSI6ImFuIiwiZ3JpZCI6Ii4zLy4zIn19
+ok
+$ git config --local remote.cds.annex-security-allow-unverified-downloads ACKTHPPT
+$ git annex get 2022-01-01.grib 
+get 2022-01-01.grib (from cds...) 
+2024-04-06 11:37:05,250 INFO Welcome to the CDS
+2024-04-06 11:37:05,251 INFO Sending request to https://cds.climate.copernicus.eu/api/v2/resources/reanalysis-era5-complete
+2024-04-06 11:37:05,340 INFO Request is queued
+2024-04-06 11:37:06,400 INFO Request is running
+2024-04-06 11:37:26,399 INFO Request is completed
+2024-04-06 11:37:26,399 INFO Downloading https://download-0017.copernicus-climate.eu/cache-compute-0017/cache/data9/adaptor.mars.external-1712396225.5545986-18258-18-822e5b91-cf60-4dbd-a808-a1253d4fe109.grib to .git/annex/tmp/URL--cds&cv1-eyJkYXRhc2V0IjoicmVhbmFs-162a71d794c333f5e04b13283421a49a (5.5M)
+  0%|          | 0.00/5.51M [00:00<?, ?B/s]  2%|▏         | 104k/5.51M [00:00<00:06, 866kB/s] 17%|█▋        | 942k/5.51M [00:00<00:00, 5.00MB/s] 57%|█████▋    | 3.15M/5.51M [00:00<00:00, 13.0MB/s] 99%|█████████▉| 5.48M/5.51M [00:00<00:00, 17.3MB/s]                                                    2024-04-06 11:37:27,322 INFO Download rate 6M/s
+ok
+(recording state in git...)
+$ git annex migrate
+migrate 2022-01-01.grib (checksum...) ok
+(recording state in git...)
+$ ls -l
+[...] 2022-01-01.grib -> .git/annex/objects/KJ/6K/MD5E-s5774880--94a848eefd02d72952c8541c52a93550.grib/MD5E-s5774880--94a848eefd02d72952c8541c52a93550.grib
+$ git annex whereis 2022-01-01.grib 
+whereis 2022-01-01.grib (1 copy) 
+  	5dfef0c9-8e18-4ea2-9ee1-646830b5749b --  [here]
+ok
+$ git annex drop 2022-01-01.grib 
+drop 2022-01-01.grib (unsafe) 
+  Could only verify the existence of 0 out of 1 necessary copy
+
+  Rather than dropping this file, try using: git annex move
+
+  (Use --force to override this check, or adjust numcopies.)
+failed
+drop: 1 failed
+"""]]
+
+I know that this is sort of abusing the URL handling in git-annex, but it was super easy to implement. You recommended me to use SETSTATE/GETSTATE from the external special remote protocol instead already at some point, but I didn't get around to reworking it for that yet.
+
+### Have you had any luck using git-annex before? (Sometimes we get tired of reading bug reports all day and a lil' positive end note does wonders)
+
+Yes! It is absolutely great, thank you for it.

Added a comment: Possible simplified scenario
diff --git a/doc/bugs/Fails_to_drop_key_on_windows___40__Access_denied__41__/comment_2_cdedb5512dc9c27a88632e963a27a389._comment b/doc/bugs/Fails_to_drop_key_on_windows___40__Access_denied__41__/comment_2_cdedb5512dc9c27a88632e963a27a389._comment
new file mode 100644
index 0000000000..1f1090c353
--- /dev/null
+++ b/doc/bugs/Fails_to_drop_key_on_windows___40__Access_denied__41__/comment_2_cdedb5512dc9c27a88632e963a27a389._comment
@@ -0,0 +1,39 @@
+[[!comment format=mdwn
+ username="jasonc"
+ nickname="mail"
+ avatar="http://cdn.libravatar.org/avatar/cb07bdfbe978aa83388d64e08a972eb2"
+ subject="Possible simplified scenario"
+ date="2024-04-03T16:48:04Z"
+ content="""
+Hello, firstly thank you for developing a really useful piece of software.  During my initial experimentation I came across what appears to be a variation of this bug, and think I've distilled it to a minimal reproducible scenario.
+
+Initialise in the usual way on an NTFS partition, then add a directory special remote (no `encryption`, no `importtree` and no `exporttree`):
+<pre>
+    git init
+    git annex init local
+    git annex initremote nextdoor type=directory directory=N:\nextdoordir encryption=none
+</pre>
+
+In my case I then added and committed the files locally, then moved them to the directory special remote and back again:
+<pre>
+    git annex add .
+    git commit --all --message=\"first commit\"
+    git annex move . --to nextdoor
+    git annex move . --from nextdoor
+</pre>
+
+This completes successfully, however repeating the last two steps a second time triggers the `permission denied (Access is denied.)` failure at the start of the bug report.
+
+Going through each part step by step:
+
+* Since NTFS is designated as a \"crippled filesystem\", the annexed objects appear to be read-write by default (no ACL modifications, no ReadOnly attribute).
+* When the files are moved away to the directory special remote (in my test, the same NTFS partition), they pick up a ReadOnly attribute in the new location, so `Archive+Compression` becomes `ReadOnly+Archive+Compression`.
+* When the files are then moved back from the directory special remote, the ReadOnly attribute persists.
+* Repeating the movement then fails, as the file cannot be dropped locally (the UNC path exists, but `DeleteFile` fails).
+
+If I remove the ReadOnly attributes and try again, the move away is successful.  Similarly if I use a networked ext4 location for the directory special remote (and NTFS locally), the same cycle of success then failure can be observed.
+
+Version information: git `git version 2.44.0.windows.1`, annex `git-annex version: 10.20240130-gad8e32c09d3ec866e0c0654cdcd146bf1aefbc5e` (installer from 2024-02-27), Windows 10 22H2
+
+If you require logs or other information, please let me know.
+"""]]

Add link to English re-recording of Yann's git-annex workshop kickoff talk @Tübix2023
diff --git a/doc/users/nobodyinperson.mdwn b/doc/users/nobodyinperson.mdwn
index 18b8cfb8f1..56a3d1ee05 100644
--- a/doc/users/nobodyinperson.mdwn
+++ b/doc/users/nobodyinperson.mdwn
@@ -10,4 +10,4 @@ I made a [Thunar plugin](https://gitlab.com/nobodyinperson/thunar-plugins) for g
 
 In an attempt to [#gitAnnexAllTheThings](https://fosstodon.org/tags/gitAnnexAllTheThings), I used git annex as a backend for a cli time tracker [annextimelog](https://pypi.org/project/annextimelog/). It has similarities with timewarrior but adresses many of its inconveniences.
 
-At the [Tübix 2023](https://www.tuebix.org/) I gave a (German) git annex workshop, of which you can find a recording of the initial talk [📹 here in the fediverse](https://tube.tchncs.de/w/db1ec5ca-94ad-4f49-a507-2124fd699ff1) and [📹 here on Odysee](https://odysee.com/@nobodyinperson:6/T%C3%BCbix2023-Yann-B%C3%BCchau-git-annex:6). 
+At the [Tübix 2023](https://www.tuebix.org/) I gave a git annex workshop, of which you can find a recording of the initial talk [📹 here (🇩🇪 German)](https://tube.tchncs.de/w/db1ec5ca-94ad-4f49-a507-2124fd699ff1) or [📹 here (English)](https://tube.tchncs.de/w/1U4vbTAhSEje3KQ1dGqvxh) in the fediverse and [📹 here on Odysee (🇩🇪 German)](https://odysee.com/@nobodyinperson:6/T%C3%BCbix2023-Yann-B%C3%BCchau-git-annex:6). 

mention reversion
diff --git a/doc/todo/multiple_-m.mdwn b/doc/todo/multiple_-m.mdwn
index c9c01b2ef9..5bcb74fe74 100644
--- a/doc/todo/multiple_-m.mdwn
+++ b/doc/todo/multiple_-m.mdwn
@@ -1,2 +1,6 @@
 git-annex sync etc -m should be able to be specified multiple times. In git
 commit, multiple -m can be used to make a multiparagraph commit. --[[Joey]]
+
+> I got this implemented, but it caused a reversion. See 
+> [[!commit a8dd85ea5a9f8515819db04b9f1d154488193e7d]]
+> for what needs to be done on this next. --[[Joey]]

Revert "multiple -m"
This reverts commit cee12f6a2fd7f90eb2aa72cd638b6bcdf45e4f92.
This commit broke git-annex init run in a repo that was cloned from a
repo with an adjusted branch checked out.
The problem is that findAdjustingCommit was not able to identify the
commit that created the adjusted branch. It seems that there is an extra
"\n" at the end of the commit message that it does not expect.
Since backwards compatability needs to be maintained, cannot just make
findAdjustingCommit accept it with the "\n". Will have to instead
have one commitTree variant that uses the old method, and use it for
adjusted branch committing.
diff --git a/Annex/AdjustedBranch.hs b/Annex/AdjustedBranch.hs
index c545930133..a7b6f03ed7 100644
--- a/Annex/AdjustedBranch.hs
+++ b/Annex/AdjustedBranch.hs
@@ -469,7 +469,7 @@ commitAdjustedTree' treesha (BasisBranch basis) parents =
 			(commitCommitterMetaData basiscommit)
 			(mkcommit cmode)
 	mkcommit cmode = Git.Branch.commitTree cmode
-		[adjustedBranchCommitMessage] parents treesha
+		adjustedBranchCommitMessage parents treesha
 
 {- This message should never be changed. -}
 adjustedBranchCommitMessage :: String
@@ -577,7 +577,7 @@ reverseAdjustedCommit commitparent adj (csha, basiscommit) origbranch
 			(commitAuthorMetaData basiscommit)
 			(commitCommitterMetaData basiscommit) $
 				Git.Branch.commitTree cmode
-					[commitMessage basiscommit]
+					(commitMessage basiscommit)
 					[commitparent] treesha
 		return (Right revadjcommit)
 
diff --git a/Annex/AdjustedBranch/Merge.hs b/Annex/AdjustedBranch/Merge.hs
index 904f4ee412..26ab0e7e3e 100644
--- a/Annex/AdjustedBranch/Merge.hs
+++ b/Annex/AdjustedBranch/Merge.hs
@@ -153,8 +153,7 @@ mergeToAdjustedBranch tomerge (origbranch, adj) mergeconfig canresolvemerge comm
 			then do
 				cmode <- annexCommitMode <$> Annex.getGitConfig
 				c <- inRepo $ Git.Branch.commitTree cmode
-					["Merged " ++ fromRef tomerge]
-					[adjmergecommit]
+					("Merged " ++ fromRef tomerge) [adjmergecommit]
 					(commitTree currentcommit)
 				inRepo $ Git.Branch.update "updating adjusted branch" currbranch c
 				propigateAdjustedCommits origbranch adj
diff --git a/Annex/Branch.hs b/Annex/Branch.hs
index bcc9ae114d..9b5365b456 100644
--- a/Annex/Branch.hs
+++ b/Annex/Branch.hs
@@ -945,9 +945,9 @@ rememberTreeishLocked treeish graftpoint jl = do
 	addedt <- inRepo $ Git.Tree.graftTree treeish graftpoint origtree
 	cmode <- annexCommitMode <$> Annex.getGitConfig
 	c <- inRepo $ Git.Branch.commitTree cmode
-		["graft"] [branchref] addedt
+		"graft" [branchref] addedt
 	c' <- inRepo $ Git.Branch.commitTree cmode
-		["graft cleanup"] [c] origtree
+		"graft cleanup" [c] origtree
 	inRepo $ Git.Branch.update' fullname c'
 	-- The tree in c' is the same as the tree in branchref,
 	-- and the index was updated to that above, so it's safe to
diff --git a/Annex/Import.hs b/Annex/Import.hs
index 2778740382..eaf41f4f79 100644
--- a/Annex/Import.hs
+++ b/Annex/Import.hs
@@ -86,7 +86,7 @@ data ImportCommitConfig = ImportCommitConfig
 	{ importCommitTracking :: Maybe Sha
 	-- ^ Current commit on the remote tracking branch.
 	, importCommitMode :: Git.Branch.CommitMode
-	, importCommitMessages :: [String]
+	, importCommitMessage :: String
 	}
 
 {- Buils a commit for an import from a special remote.
@@ -251,7 +251,7 @@ buildImportCommit' remote importcommitconfig mtrackingcommit imported@(History t
 
 	mkcommit parents tree = inRepo $ Git.Branch.commitTree
 		(importCommitMode importcommitconfig)
-		(importCommitMessages importcommitconfig)
+		(importCommitMessage importcommitconfig)
 		parents
 		tree
 
diff --git a/Annex/RemoteTrackingBranch.hs b/Annex/RemoteTrackingBranch.hs
index 06591d0918..ade303e02d 100644
--- a/Annex/RemoteTrackingBranch.hs
+++ b/Annex/RemoteTrackingBranch.hs
@@ -77,7 +77,7 @@ makeRemoteTrackingBranchMergeCommit' commitsha importedhistory treesha = do
 	cmode <- annexCommitMode <$> Annex.getGitConfig
 	inRepo $ Git.Branch.commitTree
 			cmode
-			["remote tracking branch"]
+			"remote tracking branch"
 			[commitsha, importedhistory]
 			treesha
 
diff --git a/Annex/View.hs b/Annex/View.hs
index 7372287380..b47e34564b 100644
--- a/Annex/View.hs
+++ b/Annex/View.hs
@@ -577,7 +577,7 @@ updateView view madj = do
 			cmode <- annexCommitMode <$> Annex.getGitConfig
 			let msg = "updated " ++ fromRef (branchView view madj)
 			let parent = catMaybes [oldcommit]
-			inRepo (Git.Branch.commitTree cmode [msg] parent newtree)
+			inRepo (Git.Branch.commitTree cmode msg parent newtree)
 		else return Nothing
 
 {- Diff between currently checked out branch and staged changes, and
diff --git a/CHANGELOG b/CHANGELOG
index 3a4adca108..83dfa02de9 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -13,8 +13,6 @@ git-annex (10.20240228) UNRELEASED; urgency=medium
     the same repository.
   * Windows: Fix escaping output to terminal when using old
     versions of MinTTY.
-  * sync, assist, import: Allow -m option to be specified multiple
-    times, to provide additional paragraphs for the commit message.
 
  -- Joey Hess <id@joeyh.name>  Tue, 27 Feb 2024 13:07:10 -0400
 
diff --git a/Command/FilterBranch.hs b/Command/FilterBranch.hs
index 6c565c5d29..10f03cccda 100644
--- a/Command/FilterBranch.hs
+++ b/Command/FilterBranch.hs
@@ -189,7 +189,7 @@ seek o = withOtherTmp $ \tmpdir -> do
 	liftIO $ removeWhenExistsWith removeLink tmpindex
 	cmode <- annexCommitMode <$> Annex.getGitConfig
 	cmessage <- Annex.Branch.commitMessage
-	c <- inRepo $ Git.commitTree cmode [cmessage] [] t
+	c <- inRepo $ Git.commitTree cmode cmessage [] t
 	liftIO $ putStrLn (fromRef c)
   where
 	ww = WarnUnmatchLsFiles "filter-branch"
diff --git a/Command/Import.hs b/Command/Import.hs
index f5483cc7d5..a37064eefc 100644
--- a/Command/Import.hs
+++ b/Command/Import.hs
@@ -1,6 +1,6 @@
 {- git-annex command
  -
- - Copyright 2012-2024 Joey Hess <id@joeyh.name>
+ - Copyright 2012-2021 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -70,7 +70,7 @@ data ImportOptions
 		, importToSubDir :: Maybe FilePath
 		, importContent :: Bool
 		, checkGitIgnoreOption :: CheckGitIgnore
-		, messageOption :: [String]
+		, messageOption :: Maybe String
 		}
 
 optParser :: CmdParamsDesc -> Parser ImportOptions
@@ -82,7 +82,7 @@ optParser desc = do
 		)
 	dupmode <- fromMaybe Default <$> optional duplicateModeParser
 	ic <- Command.Add.checkGitIgnoreSwitch
-	message <- many (strOption
+	message <- optional (strOption
 		( long "message" <> short 'm' <> metavar "MSG"
 		<> help "commit message"
 		))
@@ -322,8 +322,8 @@ verifyExisting key destfile (yes, no) = do
 	verifyEnoughCopiesToDrop [] key Nothing needcopies mincopies [] preverified tocheck
 		(const yes) no
 
-seekRemote :: Remote -> Branch -> Maybe TopFilePath -> Bool -> CheckGitIgnore -> [String] -> CommandSeek
-seekRemote remote branch msubdir importcontent ci importmessages = do
+seekRemote :: Remote -> Branch -> Maybe TopFilePath -> Bool -> CheckGitIgnore -> Maybe String -> CommandSeek
+seekRemote remote branch msubdir importcontent ci mimportmessage = do
 	importtreeconfig <- case msubdir of
 		Nothing -> return ImportTree
 		Just subdir ->
@@ -336,7 +336,7 @@ seekRemote remote branch msubdir importcontent ci importmessages = do
 	
 	trackingcommit <- fromtrackingbranch Git.Ref.sha
 	cmode <- annexCommitMode <$> Annex.getGitConfig
-	let importcommitconfig = ImportCommitConfig trackingcommit cmode importmessages'
+	let importcommitconfig = ImportCommitConfig trackingcommit cmode importmessage
 	let commitimport = commitRemote remote branch tb trackingcommit importtreeconfig importcommitconfig
 
 	importabletvar <- liftIO $ newTVarIO Nothing
@@ -353,9 +353,9 @@ seekRemote remote branch msubdir importcontent ci importmessages = do
 				includeCommandAction $ 
 					commitimport imported
   where
-	importmessages'
-		| null importmessages = ["import from " ++ Remote.name remote]
-		| otherwise = importmessages
+	importmessage = fromMaybe 
+		("import from " ++ Remote.name remote)
+		mimportmessage
 
 	tb = mkRemoteTrackingBranch remote branch
 
diff --git a/Command/Sync.hs b/Command/Sync.hs
index 5c4ba2ebe2..e222dd0874 100644
--- a/Command/Sync.hs
+++ b/Command/Sync.hs
@@ -1,7 +1,7 @@
 {- git-annex command
  -
  - Copyright 2011 Joachim Breitner <mail@joachim-breitner.de>
- - Copyright 2011-2024 Joey Hess <id@joeyh.name>
+ - Copyright 2011-2023 Joey Hess <id@joeyh.name>
  -

(Diff truncated)
Added a comment: support for bulk write/read/test remote
diff --git a/doc/design/external_special_remote_protocol/comment_54_0225736488f9c1e7f78a08eea8ca864d._comment b/doc/design/external_special_remote_protocol/comment_54_0225736488f9c1e7f78a08eea8ca864d._comment
new file mode 100644
index 0000000000..66c7ce087b
--- /dev/null
+++ b/doc/design/external_special_remote_protocol/comment_54_0225736488f9c1e7f78a08eea8ca864d._comment
@@ -0,0 +1,24 @@
+[[!comment format=mdwn
+ username="psxvoid"
+ avatar="http://cdn.libravatar.org/avatar/fde068fbdeabeea31e3be7aa9c55d84b"
+ subject="support for bulk write/read/test remote"
+ date="2024-04-02T06:41:25Z"
+ content="""
+Hi,
+
+I'm wondering whether there an any easy way to delay \"progress reporting\" (a.k.a. \"report progress for ALL `transfer_store` operations ONCE\", a.k.a. \"bulk transfer\") for a special remote?
+
+What I'm trying to achieve: there is an archiver called [dar](http://dar.linux.free.fr/), which I would like to implement a special remote for.
+It can write many files into a single archive and also supports incremental/differential backups.
+A one can create an archive with this utility, by providing a list of files or directories as params.
+
+The problem with the current git annex special remote API is that it does not allows to report transfer progress for ALL key/files for a special remote (e.g. with `transfer_store`), and then check the progress at **ONCE** for **ALL** files at the end of the process.
+Ideally, the protocol should have some kind of \"write test\" command to check the written archive for errors, and only then report the progress as \"successful\".
+
+What I was thinking of is to just write all files into a temporarily list during `transfer_store`, and then externally archive this list of files after `git annex copy --to dar-remote` is done. But seems like git annex will think that the process of writing files to that remote was successful, while it may not (e.g. file access error happened, or an archive was corrupted, etc).
+
+How can it be achieved? Do we need to extend git annex with another protocol extension? How difficult it may be, and where to start?
+I suppose there is no way Joey or anyone else will work on it any time soon if there is no workaround, and I have to submit a patch?
+
+P.S.: I've seen [async extensions](https://git-annex.branchable.com/design/external_special_remote_protocol/async_appendix/) but it seems like it's tied to a threads, which most likely won't allow to achieve the described goals.
+"""]]

Added a comment
diff --git a/doc/special_remotes/rclone/comment_3_d946b28311da5e3632c651f52357a2c1._comment b/doc/special_remotes/rclone/comment_3_d946b28311da5e3632c651f52357a2c1._comment
new file mode 100644
index 0000000000..2ecffcaf68
--- /dev/null
+++ b/doc/special_remotes/rclone/comment_3_d946b28311da5e3632c651f52357a2c1._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="oadams"
+ avatar="http://cdn.libravatar.org/avatar/ac166a5f89f10c4108e5150015e6751b"
+ subject="comment 3"
+ date="2024-04-02T03:56:50Z"
+ content="""
+That makes a lot of sense. Yeah, I saw that post not long after posting my last comment. Looks promising, I'll have to try it out when I set up my next remote.
+"""]]

Added a comment
diff --git a/doc/forum/New_external_special_remote_for_rclone/comment_7_4d7f07a872ef3cc2c4928dcd1580bd0a._comment b/doc/forum/New_external_special_remote_for_rclone/comment_7_4d7f07a872ef3cc2c4928dcd1580bd0a._comment
new file mode 100644
index 0000000000..fab02c082a
--- /dev/null
+++ b/doc/forum/New_external_special_remote_for_rclone/comment_7_4d7f07a872ef3cc2c4928dcd1580bd0a._comment
@@ -0,0 +1,9 @@
+[[!comment format=mdwn
+ username="d@403a635aa8eaa8bfa8613acb6a375d9e06ed7001"
+ nickname="d"
+ avatar="http://cdn.libravatar.org/avatar/b79468a0d03ec3ec7acbae547c4fa994"
+ subject="comment 7"
+ date="2024-03-27T22:11:47Z"
+ content="""
+Ah, I did not realize that! I thought I could only post on the forum for some reason.
+"""]]

fixes
diff --git a/doc/forum/New_external_special_remote_for_rclone/comment_6_36a0260f013ccbd06460dea83fe22278._comment b/doc/forum/New_external_special_remote_for_rclone/comment_6_36a0260f013ccbd06460dea83fe22278._comment
new file mode 100644
index 0000000000..23769242c0
--- /dev/null
+++ b/doc/forum/New_external_special_remote_for_rclone/comment_6_36a0260f013ccbd06460dea83fe22278._comment
@@ -0,0 +1,7 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 6"""
+ date="2024-03-27T20:00:04Z"
+ content="""
+Fixed those. (Note that you can edit the page yourself!)
+"""]]
diff --git a/doc/special_remotes/rclone.mdwn b/doc/special_remotes/rclone.mdwn
index 046630445b..12dc18e36c 100644
--- a/doc/special_remotes/rclone.mdwn
+++ b/doc/special_remotes/rclone.mdwn
@@ -32,6 +32,6 @@ the use of the [rclone special remote](https://github.com/DanielDent/git-annex-r
 Alternatively, rclone recently gained support for being used as a special
 remote on its own, without needing installation of the above program.
 For documentation on using rclone that way, see the output of
-`rclone gitannex` or [here](https://github.com/git-annex-remote-rclone/git-annex-remote-rclone).
+`rclone gitannex -h` or [here](//github.com/rclone/rclone/blob/master/cmd/gitannex/gitannex.md).
 
 See their documentation for more concrete examples.

multiple -m
sync, assist, import: Allow -m option to be specified multiple times, to
provide additional paragraphs for the commit message.
The option parser didn't allow multiple -m before, so there is no risk of
behavior change breaking something that was for some reason using multiple
-m already.
Pass through to git commands, so that the method used to assemble the
paragrahs is whatever git does. Which might conceivably change in the
future.
Note that git commit-tree has supported -m since git 1.7.7. commitTree
was probably not using it since it predates that version. Since the
configure script prevents building git-annex with git older than 2.1,
there is no risk that it's not supported now.
Sponsored-by: Nicholas Golder-Manning on Patreon
diff --git a/Annex/AdjustedBranch.hs b/Annex/AdjustedBranch.hs
index a7b6f03ed7..c545930133 100644
--- a/Annex/AdjustedBranch.hs
+++ b/Annex/AdjustedBranch.hs
@@ -469,7 +469,7 @@ commitAdjustedTree' treesha (BasisBranch basis) parents =
 			(commitCommitterMetaData basiscommit)
 			(mkcommit cmode)
 	mkcommit cmode = Git.Branch.commitTree cmode
-		adjustedBranchCommitMessage parents treesha
+		[adjustedBranchCommitMessage] parents treesha
 
 {- This message should never be changed. -}
 adjustedBranchCommitMessage :: String
@@ -577,7 +577,7 @@ reverseAdjustedCommit commitparent adj (csha, basiscommit) origbranch
 			(commitAuthorMetaData basiscommit)
 			(commitCommitterMetaData basiscommit) $
 				Git.Branch.commitTree cmode
-					(commitMessage basiscommit)
+					[commitMessage basiscommit]
 					[commitparent] treesha
 		return (Right revadjcommit)
 
diff --git a/Annex/AdjustedBranch/Merge.hs b/Annex/AdjustedBranch/Merge.hs
index 26ab0e7e3e..904f4ee412 100644
--- a/Annex/AdjustedBranch/Merge.hs
+++ b/Annex/AdjustedBranch/Merge.hs
@@ -153,7 +153,8 @@ mergeToAdjustedBranch tomerge (origbranch, adj) mergeconfig canresolvemerge comm
 			then do
 				cmode <- annexCommitMode <$> Annex.getGitConfig
 				c <- inRepo $ Git.Branch.commitTree cmode
-					("Merged " ++ fromRef tomerge) [adjmergecommit]
+					["Merged " ++ fromRef tomerge]
+					[adjmergecommit]
 					(commitTree currentcommit)
 				inRepo $ Git.Branch.update "updating adjusted branch" currbranch c
 				propigateAdjustedCommits origbranch adj
diff --git a/Annex/Branch.hs b/Annex/Branch.hs
index 9b5365b456..bcc9ae114d 100644
--- a/Annex/Branch.hs
+++ b/Annex/Branch.hs
@@ -945,9 +945,9 @@ rememberTreeishLocked treeish graftpoint jl = do
 	addedt <- inRepo $ Git.Tree.graftTree treeish graftpoint origtree
 	cmode <- annexCommitMode <$> Annex.getGitConfig
 	c <- inRepo $ Git.Branch.commitTree cmode
-		"graft" [branchref] addedt
+		["graft"] [branchref] addedt
 	c' <- inRepo $ Git.Branch.commitTree cmode
-		"graft cleanup" [c] origtree
+		["graft cleanup"] [c] origtree
 	inRepo $ Git.Branch.update' fullname c'
 	-- The tree in c' is the same as the tree in branchref,
 	-- and the index was updated to that above, so it's safe to
diff --git a/Annex/Import.hs b/Annex/Import.hs
index eaf41f4f79..2778740382 100644
--- a/Annex/Import.hs
+++ b/Annex/Import.hs
@@ -86,7 +86,7 @@ data ImportCommitConfig = ImportCommitConfig
 	{ importCommitTracking :: Maybe Sha
 	-- ^ Current commit on the remote tracking branch.
 	, importCommitMode :: Git.Branch.CommitMode
-	, importCommitMessage :: String
+	, importCommitMessages :: [String]
 	}
 
 {- Buils a commit for an import from a special remote.
@@ -251,7 +251,7 @@ buildImportCommit' remote importcommitconfig mtrackingcommit imported@(History t
 
 	mkcommit parents tree = inRepo $ Git.Branch.commitTree
 		(importCommitMode importcommitconfig)
-		(importCommitMessage importcommitconfig)
+		(importCommitMessages importcommitconfig)
 		parents
 		tree
 
diff --git a/Annex/RemoteTrackingBranch.hs b/Annex/RemoteTrackingBranch.hs
index ade303e02d..06591d0918 100644
--- a/Annex/RemoteTrackingBranch.hs
+++ b/Annex/RemoteTrackingBranch.hs
@@ -77,7 +77,7 @@ makeRemoteTrackingBranchMergeCommit' commitsha importedhistory treesha = do
 	cmode <- annexCommitMode <$> Annex.getGitConfig
 	inRepo $ Git.Branch.commitTree
 			cmode
-			"remote tracking branch"
+			["remote tracking branch"]
 			[commitsha, importedhistory]
 			treesha
 
diff --git a/Annex/View.hs b/Annex/View.hs
index b47e34564b..7372287380 100644
--- a/Annex/View.hs
+++ b/Annex/View.hs
@@ -577,7 +577,7 @@ updateView view madj = do
 			cmode <- annexCommitMode <$> Annex.getGitConfig
 			let msg = "updated " ++ fromRef (branchView view madj)
 			let parent = catMaybes [oldcommit]
-			inRepo (Git.Branch.commitTree cmode msg parent newtree)
+			inRepo (Git.Branch.commitTree cmode [msg] parent newtree)
 		else return Nothing
 
 {- Diff between currently checked out branch and staged changes, and
diff --git a/CHANGELOG b/CHANGELOG
index 83dfa02de9..3a4adca108 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -13,6 +13,8 @@ git-annex (10.20240228) UNRELEASED; urgency=medium
     the same repository.
   * Windows: Fix escaping output to terminal when using old
     versions of MinTTY.
+  * sync, assist, import: Allow -m option to be specified multiple
+    times, to provide additional paragraphs for the commit message.
 
  -- Joey Hess <id@joeyh.name>  Tue, 27 Feb 2024 13:07:10 -0400
 
diff --git a/Command/FilterBranch.hs b/Command/FilterBranch.hs
index 10f03cccda..6c565c5d29 100644
--- a/Command/FilterBranch.hs
+++ b/Command/FilterBranch.hs
@@ -189,7 +189,7 @@ seek o = withOtherTmp $ \tmpdir -> do
 	liftIO $ removeWhenExistsWith removeLink tmpindex
 	cmode <- annexCommitMode <$> Annex.getGitConfig
 	cmessage <- Annex.Branch.commitMessage
-	c <- inRepo $ Git.commitTree cmode cmessage [] t
+	c <- inRepo $ Git.commitTree cmode [cmessage] [] t
 	liftIO $ putStrLn (fromRef c)
   where
 	ww = WarnUnmatchLsFiles "filter-branch"
diff --git a/Command/Import.hs b/Command/Import.hs
index a37064eefc..f5483cc7d5 100644
--- a/Command/Import.hs
+++ b/Command/Import.hs
@@ -1,6 +1,6 @@
 {- git-annex command
  -
- - Copyright 2012-2021 Joey Hess <id@joeyh.name>
+ - Copyright 2012-2024 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -70,7 +70,7 @@ data ImportOptions
 		, importToSubDir :: Maybe FilePath
 		, importContent :: Bool
 		, checkGitIgnoreOption :: CheckGitIgnore
-		, messageOption :: Maybe String
+		, messageOption :: [String]
 		}
 
 optParser :: CmdParamsDesc -> Parser ImportOptions
@@ -82,7 +82,7 @@ optParser desc = do
 		)
 	dupmode <- fromMaybe Default <$> optional duplicateModeParser
 	ic <- Command.Add.checkGitIgnoreSwitch
-	message <- optional (strOption
+	message <- many (strOption
 		( long "message" <> short 'm' <> metavar "MSG"
 		<> help "commit message"
 		))
@@ -322,8 +322,8 @@ verifyExisting key destfile (yes, no) = do
 	verifyEnoughCopiesToDrop [] key Nothing needcopies mincopies [] preverified tocheck
 		(const yes) no
 
-seekRemote :: Remote -> Branch -> Maybe TopFilePath -> Bool -> CheckGitIgnore -> Maybe String -> CommandSeek
-seekRemote remote branch msubdir importcontent ci mimportmessage = do
+seekRemote :: Remote -> Branch -> Maybe TopFilePath -> Bool -> CheckGitIgnore -> [String] -> CommandSeek
+seekRemote remote branch msubdir importcontent ci importmessages = do
 	importtreeconfig <- case msubdir of
 		Nothing -> return ImportTree
 		Just subdir ->
@@ -336,7 +336,7 @@ seekRemote remote branch msubdir importcontent ci mimportmessage = do
 	
 	trackingcommit <- fromtrackingbranch Git.Ref.sha
 	cmode <- annexCommitMode <$> Annex.getGitConfig
-	let importcommitconfig = ImportCommitConfig trackingcommit cmode importmessage
+	let importcommitconfig = ImportCommitConfig trackingcommit cmode importmessages'
 	let commitimport = commitRemote remote branch tb trackingcommit importtreeconfig importcommitconfig
 
 	importabletvar <- liftIO $ newTVarIO Nothing
@@ -353,9 +353,9 @@ seekRemote remote branch msubdir importcontent ci mimportmessage = do
 				includeCommandAction $ 
 					commitimport imported
   where
-	importmessage = fromMaybe 
-		("import from " ++ Remote.name remote)
-		mimportmessage
+	importmessages'
+		| null importmessages = ["import from " ++ Remote.name remote]
+		| otherwise = importmessages
 
 	tb = mkRemoteTrackingBranch remote branch
 
diff --git a/Command/Sync.hs b/Command/Sync.hs
index e222dd0874..5c4ba2ebe2 100644
--- a/Command/Sync.hs
+++ b/Command/Sync.hs
@@ -1,7 +1,7 @@
 {- git-annex command
  -
  - Copyright 2011 Joachim Breitner <mail@joachim-breitner.de>
- - Copyright 2011-2023 Joey Hess <id@joeyh.name>
+ - Copyright 2011-2024 Joey Hess <id@joeyh.name>
  -

(Diff truncated)
Added a comment
diff --git a/doc/forum/New_external_special_remote_for_rclone/comment_5_cf45b8efbb296013493e7756cdfcf291._comment b/doc/forum/New_external_special_remote_for_rclone/comment_5_cf45b8efbb296013493e7756cdfcf291._comment
new file mode 100644
index 0000000000..635194bcc9
--- /dev/null
+++ b/doc/forum/New_external_special_remote_for_rclone/comment_5_cf45b8efbb296013493e7756cdfcf291._comment
@@ -0,0 +1,24 @@
+[[!comment format=mdwn
+ username="d@403a635aa8eaa8bfa8613acb6a375d9e06ed7001"
+ nickname="d"
+ avatar="http://cdn.libravatar.org/avatar/b79468a0d03ec3ec7acbae547c4fa994"
+ subject="comment 5"
+ date="2024-03-27T13:26:34Z"
+ content="""
+> Looking at the docs at <https://github.com/git-annex-remote-rclone/git-annex-remote-rclone> there are a lot of options for different directory layouts. Do you have any plans to support those? It seems it might be useful for migration of existing special remotes, if nothing else.
+
+This wasn't on my roadmap, but I think it's worth prioritizing. It would enable users to migrate between these special remote implementations without requiring double the storage space on the remote and a proportional amount of patience. I've added this to milestone 2 in my [tracking comment](https://github.com/rclone/rclone/issues/7625#issuecomment-1951403856) on GitHub.
+
+> I guess that \"rcloneprefix=\" in this is the same as \"prefix=\" in git-annex-remote-rclone?
+
+Correct. I intentionally chose to start all config names with \"rclone\". I think this makes it a little easier for users to grok the initremote command, since other key-value pairs like `encryption=hybrid` also show up there. I suppose I could also support config names without the rclone prefix without much trouble, and it might make it easier for users to migrate. I'll have to mull on that. Also added to the tracking comment linked above.
+
+> I've added a mention of this to rclone.
+
+Awesome! Thanks for adding my project! Two edits:
+
+* The command should be `rclone gitannex -h`. Without `-h`, it will start speaking the external special remote protocol at the user ;)
+* The link should be <https://github.com/rclone/rclone/blob/master/cmd/gitannex/gitannex.md> -- looks like you accidentally pasted the link for git-annex-remote-rclone.
+
+
+"""]]

todo
diff --git a/doc/todo/multiple_-m.mdwn b/doc/todo/multiple_-m.mdwn
new file mode 100644
index 0000000000..c9c01b2ef9
--- /dev/null
+++ b/doc/todo/multiple_-m.mdwn
@@ -0,0 +1,2 @@
+git-annex sync etc -m should be able to be specified multiple times. In git
+commit, multiple -m can be used to make a multiparagraph commit. --[[Joey]]

Added a comment
diff --git a/doc/todo/external_special_remotes_not_using_git-annex-remote_in_name/comment_1_8bb17f1a8297b500dcc5bdf9412a5d6c._comment b/doc/todo/external_special_remotes_not_using_git-annex-remote_in_name/comment_1_8bb17f1a8297b500dcc5bdf9412a5d6c._comment
new file mode 100644
index 0000000000..e5e87eaf0b
--- /dev/null
+++ b/doc/todo/external_special_remotes_not_using_git-annex-remote_in_name/comment_1_8bb17f1a8297b500dcc5bdf9412a5d6c._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="yarikoptic"
+ avatar="http://cdn.libravatar.org/avatar/f11e9c84cb18d26a1748c33b48c924b4"
+ subject="comment 1"
+ date="2024-03-26T19:13:15Z"
+ content="""
+my 1c: since `git-annex` is pretty much following git's own `git-remote-` paradigm here, I think it is ok to require `git-annex-remote-` to even simply be able to check what are the available `git-annex` remotes etc. Providing a shim to pass through to some tool specific interface is IMHO trivial enough. And as you outline, such approach also helps to avoid a security concern.
+"""]]

comment
diff --git a/doc/forum/New_external_special_remote_for_rclone/comment_4_9a23f4e9d673b4dcf60b3425fa4df4aa._comment b/doc/forum/New_external_special_remote_for_rclone/comment_4_9a23f4e9d673b4dcf60b3425fa4df4aa._comment
new file mode 100644
index 0000000000..57a410e607
--- /dev/null
+++ b/doc/forum/New_external_special_remote_for_rclone/comment_4_9a23f4e9d673b4dcf60b3425fa4df4aa._comment
@@ -0,0 +1,16 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 4"""
+ date="2024-03-26T17:39:25Z"
+ content="""
+Looking at the docs at
+<https://github.com/git-annex-remote-rclone/git-annex-remote-rclone>
+there are a lot of options for different directory layouts. Do you have any
+plans to support those? It seems it might be useful for migration of
+existing special remotes, if nothing else.
+
+I guess that "rcloneprefix=" in this is the same as "prefix="
+in git-annex-remote-rclone?
+
+I've added a mention of this to [[special_remotes/rclone]].
+"""]]

update for new rclone gitannex command
diff --git a/doc/forum/New_external_special_remote_for_rclone/comment_3_4412a5a25ac933de71772afd23cd66bd._comment b/doc/forum/New_external_special_remote_for_rclone/comment_3_4412a5a25ac933de71772afd23cd66bd._comment
new file mode 100644
index 0000000000..1a4bf82eb6
--- /dev/null
+++ b/doc/forum/New_external_special_remote_for_rclone/comment_3_4412a5a25ac933de71772afd23cd66bd._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 3"""
+ date="2024-03-26T17:38:07Z"
+ content="""
+See [[todo/external_special_remotes_not_using_git-annex-remote_in_name]]
+for possible changes in git-annex related to this.
+"""]]
diff --git a/doc/special_remotes/rclone.mdwn b/doc/special_remotes/rclone.mdwn
index 2afbe93899..046630445b 100644
--- a/doc/special_remotes/rclone.mdwn
+++ b/doc/special_remotes/rclone.mdwn
@@ -24,7 +24,14 @@ the time of writing, this includes the following services:
   * Yandex Disk
   * The local filesystem
 
-That list is regularly expanding. git-annex supports all of those through
+That list is regularly expanding. 
+
+git-annex supports all of those through
 the use of the [rclone special remote](https://github.com/DanielDent/git-annex-remote-rclone).
 
+Alternatively, rclone recently gained support for being used as a special
+remote on its own, without needing installation of the above program.
+For documentation on using rclone that way, see the output of
+`rclone gitannex` or [here](https://github.com/git-annex-remote-rclone/git-annex-remote-rclone).
+
 See their documentation for more concrete examples.
diff --git a/doc/todo/external_special_remotes_not_using_git-annex-remote_in_name.mdwn b/doc/todo/external_special_remotes_not_using_git-annex-remote_in_name.mdwn
new file mode 100644
index 0000000000..5083acc733
--- /dev/null
+++ b/doc/todo/external_special_remotes_not_using_git-annex-remote_in_name.mdwn
@@ -0,0 +1,29 @@
+rclone now supports being run as a git-annex special remote natively
+see <https://github.com/rclone/rclone/pull/7654>. "rclone gitannex"
+is the command to run. But git-annex needs a git-annex-remote-rclone or similar,
+so they are shipping a git-annex-remote-rclone-builtin symlink to rclone,
+and when run under that name it behaves as if "rclone gitannex" were run.
+
+So in this case, the need for "git-annex-remote-foo" is complicating an
+upstream project that has gone out of its way to support git-annex. Not ideal.
+
+From the pull request, @dmcardle wrote:
+
+> My taste would be to implement a more generic mechanism rather than adding a special case for rclone gitannex. 
+> What if externaltype could be repeated, so that git annex initremote MyRemote type=external externaltype=rclone >
+> externaltype=gitannex ... would cause git-annex to exec rclone with the additional gitannex arg?
+
+But, that seems to present a security problem. Consider an attacker who runs
+`git-annex initremote foo type=external autoenable=true externaltype=rm externaltype=/foo`
+
+My conclusion is that git-annex can't provide a generic way to run a different
+command for an external special remote. Any such commands need to be
+whitelisted in some way. And if they're whitelisted, it seems better to not
+require the user to enter additional parameters at all.
+
+So one way would be to make "git-annex initremote foo type=external externaltype=rclone-builtin"
+run "rclone gitannex".
+
+Or, git-annex add an internal rclone special remote, that is just
+a wrapper around the external special remote, that makes it use
+"rclone gitannex". "git-annex initremote foo type=rclone" --[[Joey]]

Added a comment
diff --git a/doc/forum/New_external_special_remote_for_rclone/comment_2_4057e28ed32aeab048f7c97a733f180f._comment b/doc/forum/New_external_special_remote_for_rclone/comment_2_4057e28ed32aeab048f7c97a733f180f._comment
new file mode 100644
index 0000000000..ad8c8cb048
--- /dev/null
+++ b/doc/forum/New_external_special_remote_for_rclone/comment_2_4057e28ed32aeab048f7c97a733f180f._comment
@@ -0,0 +1,11 @@
+[[!comment format=mdwn
+ username="d@403a635aa8eaa8bfa8613acb6a375d9e06ed7001"
+ nickname="d"
+ avatar="http://cdn.libravatar.org/avatar/b79468a0d03ec3ec7acbae547c4fa994"
+ subject="comment 2"
+ date="2024-03-26T14:07:19Z"
+ content="""
+This has now been merged into rclone, but it's not yet shipped in a release. It is now a subcommand of rclone (`rclone gitannex`) rather than a standalone command. When invoked with `-h`, it prints some rough installation instructions, which are also available here: <https://github.com/rclone/rclone/blob/master/cmd/gitannex/gitannex.md>.
+
+As for the concurrent transfers problem, I was told by rclone's maintainer that on backends where partially uploaded files are visible, the backend automatically writes to a temp location and renames when the transfer is complete [[GitHub thread](https://github.com/rclone/rclone/pull/7654#discussion_r1512964399)]. I want to delve into this a little more and do something in `rclone gitannex` when the backend does not have this two-step-transfer property (maybe it should print a warning, or maybe just abort).
+"""]]

link to commit
diff --git a/doc/todo/v11_changes.mdwn b/doc/todo/v11_changes.mdwn
index 4b605034a3..79f5647d1b 100644
--- a/doc/todo/v11_changes.mdwn
+++ b/doc/todo/v11_changes.mdwn
@@ -17,6 +17,8 @@ version.
   interoperate with old git-annex versions that only lock the old lock file
   and not the new one.
 
+  See [[!commit f04d9574d6a56cfdf034a974f9714f0a3b8c49fa]].
+
   (Note that the old lock file should still be deleted when cleaning up the
   new lock file, to make sure that all the old lock files get deleted.)
 

fix transfer lock file for Download to not include uuid
While redundant concurrent transfers were already prevented in most
cases, it failed to prevent the case where two different repositories were
sending the same content to the same repository. By removing the uuid
from the transfer lock file for Download transfers, one repository
sending content will block the other one from also sending the same
content.
In order to interoperate with old git-annex, the old lock file is still
locked, as well as locking the new one. That added a lot of extra code
and work, and the plan is to eventually stop locking the old lock file,
at some point in time when an old git-annex process is unlikely to be
running at the same time.
Note that in the case of 2 repositories both doing eg
`git-annex copy foo --to origin`
the output is not that great:
copy b (to origin...)
transfer already in progress, or unable to take transfer lock
git-annex: transfer already in progress, or unable to take transfer lock
97% 966.81 MiB 534 GiB/s 0sp2pstdio: 1 failed
Lost connection (fd:14: hPutBuf: resource vanished (Broken pipe))
Transfer failed
Perhaps that output could be cleaned up? Anyway, it's a lot better than letting
the redundant transfer happen and then failing with an obscure error about
a temp file, which is what it did before. And it seems users don't often
try to do this, since nobody ever reported this bug to me before.
(The "97%" there is actually how far along the *other* transfer is.)
Sponsored-by: Joshua Antonishen on Patreon
diff --git a/Annex/Transfer.hs b/Annex/Transfer.hs
index 501faac1ed..4235dfcd8d 100644
--- a/Annex/Transfer.hs
+++ b/Annex/Transfer.hs
@@ -1,6 +1,6 @@
 {- git-annex transfers
  -
- - Copyright 2012-2023 Joey Hess <id@joeyh.name>
+ - Copyright 2012-2024 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -128,9 +128,10 @@ runTransfer' ignorelock t eventualbackend afile stalldetection retrydecider tran
   where
 	go = do
 		info <- liftIO $ startTransferInfo afile
-		(meter, tfile, createtfile, metervar) <- mkProgressUpdater t info
+		(tfile, lckfile, moldlckfile) <- fromRepo $ transferFileAndLockFile t
+		(meter, createtfile, metervar) <- mkProgressUpdater t info tfile
 		mode <- annexFileMode
-		(lck, inprogress) <- prep tfile createtfile mode
+		(lck, inprogress) <- prep lckfile moldlckfile createtfile mode
 		if inprogress && not ignorelock
 			then do
 				warning "transfer already in progress, or unable to take transfer lock"
@@ -139,51 +140,75 @@ runTransfer' ignorelock t eventualbackend afile stalldetection retrydecider tran
 				v <- retry 0 info metervar $
 					detectStallsAndSuggestConfig stalldetection metervar $
 						transferaction meter
-				liftIO $ cleanup tfile lck
+				liftIO $ cleanup tfile lckfile moldlckfile lck
 				if observeBool v
 					then removeFailedTransfer t
 					else recordFailedTransfer t info
 				return v
 	
-	prep :: RawFilePath -> Annex () -> ModeSetter -> Annex (Maybe LockHandle, Bool)
+	prep :: RawFilePath -> Maybe RawFilePath -> Annex () -> ModeSetter -> Annex (Maybe (LockHandle, Maybe LockHandle), Bool)
 #ifndef mingw32_HOST_OS
-	prep tfile createtfile mode = catchPermissionDenied (const prepfailed) $ do
-		let lck = transferLockFile tfile
-		createAnnexDirectory $ P.takeDirectory lck
-		tryLockExclusive (Just mode) lck >>= \case
+	prep lckfile moldlckfile createtfile mode = catchPermissionDenied (const prepfailed) $ do
+		createAnnexDirectory $ P.takeDirectory lckfile
+		tryLockExclusive (Just mode) lckfile >>= \case
 			Nothing -> return (Nothing, True)
 			-- Since the lock file is removed in cleanup,
 			-- there's a race where different processes
 			-- may have a deleted and a new version of the same
 			-- lock file open. checkSaneLock guards against
 			-- that.
-			Just lockhandle -> ifM (checkSaneLock lck lockhandle)
-				( do
-					createtfile
-					return (Just lockhandle, False)
+			Just lockhandle -> ifM (checkSaneLock lckfile lockhandle)
+				( case moldlckfile of
+					Nothing -> do
+						createtfile
+						return (Just (lockhandle, Nothing), False)
+					Just oldlckfile -> do
+						createAnnexDirectory $ P.takeDirectory oldlckfile
+						tryLockExclusive (Just mode) oldlckfile >>= \case
+							Nothing -> do
+								liftIO $ dropLock lockhandle
+								return (Nothing, True)
+							Just oldlockhandle -> ifM (checkSaneLock oldlckfile oldlockhandle)
+								( do
+									createtfile
+									return (Just (lockhandle, Just oldlockhandle), False)
+								, do
+									liftIO $ dropLock oldlockhandle
+									liftIO $ dropLock lockhandle
+									return (Nothing, True)
+								)
 				, do
 					liftIO $ dropLock lockhandle
 					return (Nothing, True)
 				)
 #else
-	prep tfile createtfile _mode = catchPermissionDenied (const prepfailed) $ do
-		let lck = transferLockFile tfile
-		createAnnexDirectory $ P.takeDirectory lck
-		catchMaybeIO (liftIO $ lockExclusive lck) >>= \case
-			Nothing -> return (Nothing, False)
-			Just Nothing -> return (Nothing, False)
-			Just (Just lockhandle) -> do
-				createtfile
-				return (Just lockhandle, False)
+	prep lckfile moldlckfile createtfile _mode = catchPermissionDenied (const prepfailed) $ do
+		createAnnexDirectory $ P.takeDirectory lckfile
+		catchMaybeIO (liftIO $ lockExclusive lckfile) >>= \case
+			Just (Just lockhandle) -> case moldlckfile of
+				Nothing -> do
+					createtfile
+					return (Just (lockhandle, Nothing), False)
+				Just oldlckfile -> do
+					createAnnexDirectory $ P.takeDirectory oldlckfile
+					catchMaybeIO (liftIO $ lockExclusive oldlckfile) >>= \case
+						Just (Just oldlockhandle) -> do
+							createtfile
+							return (Just (lockhandle, Just oldlockhandle), False)
+						_ -> do
+							liftIO $ dropLock lockhandle
+							return (Nothing, False)
+			_ -> return (Nothing, False)
 #endif
 	prepfailed = return (Nothing, False)
 
-	cleanup _ Nothing = noop
-	cleanup tfile (Just lockhandle) = do
-		let lck = transferLockFile tfile
+	cleanup _ _ _ Nothing = noop
+	cleanup tfile lckfile moldlckfile (Just (lockhandle, moldlockhandle)) = do
 		void $ tryIO $ R.removeLink tfile
 #ifndef mingw32_HOST_OS
-		void $ tryIO $ R.removeLink lck
+		void $ tryIO $ R.removeLink lckfile
+		maybe noop (void . tryIO . R.removeLink) moldlckfile
+		maybe noop dropLock moldlockhandle
 		dropLock lockhandle
 #else
 		{- Windows cannot delete the lockfile until the lock
@@ -191,8 +216,10 @@ runTransfer' ignorelock t eventualbackend afile stalldetection retrydecider tran
 		 - process that takes the lock before it's removed,
 		 - so ignore failure to remove.
 		 -}
+		maybe noop dropLock moldlockhandle
 		dropLock lockhandle
-		void $ tryIO $ R.removeLink lck
+		void $ tryIO $ R.removeLink lckfile
+		maybe noop (void . tryIO . R.removeLink) moldlckfile
 #endif
 
 	retry numretries oldinfo metervar run =
diff --git a/Assistant/Threads/TransferPoller.hs b/Assistant/Threads/TransferPoller.hs
index befbfadfe1..067bd0b022 100644
--- a/Assistant/Threads/TransferPoller.hs
+++ b/Assistant/Threads/TransferPoller.hs
@@ -45,7 +45,7 @@ transferPollerThread = namedThread "TransferPoller" $ do
 		{- Otherwise, this code polls the upload progress
 		 - by reading the transfer info file. -}
 		| otherwise = do
-			let f = transferFile t g
+			let (f, _, _) = transferFileAndLockFile t g
 			mi <- liftIO $ catchDefaultIO Nothing $
 				readTransferInfoFile Nothing (fromRawFilePath f)
 			maybe noop (newsize t info . bytesComplete) mi
diff --git a/CHANGELOG b/CHANGELOG
index 6f8042f2bd..4bc0f30158 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -7,6 +7,10 @@ git-annex (10.20240228) UNRELEASED; urgency=medium
   * Added dependency on unbounded-delays.
   * reregisterurl: New command that can change an url from being
     used by a special remote to being used by the web remote.
+  * Bugfix: While redundant concurrent transfers were already
+    prevented in most cases, it failed to prevent the case where
+    two different repositories were sending the same content to
+    the same repository.
 
  -- Joey Hess <id@joeyh.name>  Tue, 27 Feb 2024 13:07:10 -0400
 
diff --git a/Logs/Transfer.hs b/Logs/Transfer.hs
index f62a74c13d..78fc999b60 100644
--- a/Logs/Transfer.hs
+++ b/Logs/Transfer.hs
@@ -1,6 +1,6 @@
 {- git-annex transfer information files and lock files
  -
- - Copyright 2012-2021 Joey Hess <id@joeyh.name>
+ - Copyright 2012-2024 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -56,26 +56,24 @@ percentComplete t info =
 		<*> Just (fromMaybe 0 $ bytesComplete info)
 
 {- Generates a callback that can be called as transfer progresses to update
- - the transfer info file. Also returns the file it'll be updating, 
- - an action that sets up the file with appropriate permissions,
- - which should be run after locking the transfer lock file, but
- - before using the callback, and a TVar that can be used to read
- - the number of bytes processed so far. -}
-mkProgressUpdater :: Transfer -> TransferInfo -> Annex (MeterUpdate, RawFilePath, Annex (), TVar (Maybe BytesProcessed))
-mkProgressUpdater t info = do
-	tfile <- fromRepo $ transferFile t
+ - the transfer info file. Also returns an action that sets up the file with
+ - appropriate permissions, which should be run after locking the transfer
+ - lock file, but before using the callback, and a TVar that can be used to
+ - read the number of bytes processed so far. -}
+mkProgressUpdater :: Transfer -> TransferInfo -> RawFilePath -> Annex (MeterUpdate, Annex (), TVar (Maybe BytesProcessed))
+mkProgressUpdater t info tfile = do
 	let createtfile = void $ tryNonAsync $ writeTransferInfoFile info tfile
 	tvar <- liftIO $ newTVarIO Nothing
 	loggedtvar <- liftIO $ newTVarIO 0
-	return (liftIO . updater (fromRawFilePath tfile) tvar loggedtvar, tfile, createtfile, tvar)
+	return (liftIO . updater (fromRawFilePath tfile) tvar loggedtvar, createtfile, tvar)
   where

(Diff truncated)
Add news of git-annex merch from hellotux.com
diff --git a/doc/news/git-annex_shirts_now_available_at_hellotux.com.mdwn b/doc/news/git-annex_shirts_now_available_at_hellotux.com.mdwn
new file mode 100644
index 0000000000..6a514935f6
--- /dev/null
+++ b/doc/news/git-annex_shirts_now_available_at_hellotux.com.mdwn
@@ -0,0 +1,3 @@
+Since 24.03.2024, [hellotux.com](https://www.hellotux.com/git-annex) now offers git-annex-branded shirts, hoodies and backpacks. Just in time, right before the [distribits](https://distribits.live/) meeting.
+
+This cooperation was initiated by [Yann Büchau (nobodyinperson)](https://git-annex.branchable.com/users/nobodyinperson/).

Add link to hellotux.com git-annex shirts
diff --git a/doc/logo.mdwn b/doc/logo.mdwn
index ae006ce3a0..401e7fae7f 100644
--- a/doc/logo.mdwn
+++ b/doc/logo.mdwn
@@ -1,5 +1,7 @@
 Variants of the git-annex logo.
 
+👕 Since March 2024 you can buy shirts, hoodies and backpacks with the git annex logo over at [hellotux.com](https://www.hellotux.com/git-annex).
+
 [[logo_small.png]]
 
 [[logo.svg]]

bug
diff --git a/doc/bugs/redundant_transfer_not_prevented.mdwn b/doc/bugs/redundant_transfer_not_prevented.mdwn
new file mode 100644
index 0000000000..b8b017455d
--- /dev/null
+++ b/doc/bugs/redundant_transfer_not_prevented.mdwn
@@ -0,0 +1,51 @@
+Copying the same file from 2 repositories into another repository at the
+same time, the redundant transfer is not prevented.
+
+This was surprising to me! I would have expected it to prevent starting an
+upload when another transfer of that key is already running. The same as
+it does when starting 2 copies of the same file to the same remote.
+
+I'm easily able to reproduce this in a bench test with 2 clones of a repo.
+
+    git init a
+    cd a
+    git-annex init
+    dd if=/dev/urandom of=big bs=1M count=1000
+    git-annex add
+    git commit -m add
+    cd ..
+    git clone a b
+    git clone a c
+    cd b; git-annex get
+    cd ../c; git-annex move --from orgin
+    (cd b; git-annex copy --to origin) &
+    (cd c; git-annex copy --to origin) &
+
+Aha... Looking at the code, this seems like a fundamental oversight.
+The `transferFile` depends on the uuid of the remote being transfered
+to/from, so there are two different ones in this case. And the transfer
+lock file that is checked derives from those files, so there are two
+seperate lock files.
+
+That is actually what we want when eg, uploading content from the same
+repo into two different repos. It would not do for an upload to one repo
+to block uploading to another repo. So a per-uuid lock file makes sense for
+uploads. But not for downloads.
+
+It seems that it does make sense to have different transfer information
+files for downloads from different uuids. Because the filename is parsed
+to determine the uuid.
+
+Perhaps the fix is just for `transferLockFile` to take a Transfer parameter,
+and return the same lock file for all Downloads of a key, no matter the
+uuid.
+
+There is the issue that renaming the lock file would break interoperability 
+with old git-annex. In this case, since this bug prevents noticing multiple
+downloads from different uuids, interoperability would only prevent
+noticing multiple downloads from the same uuid. Which is not a great
+behavior to break either, even if it would usually only break transiently.
+
+Of course that could be avoided by keeping the current lock file, and
+adding a second level lock file.
+--[[Joey]]

comment
diff --git a/doc/special_remotes/rclone/comment_2_e5babe3bc4535946ee04e8339403fda4._comment b/doc/special_remotes/rclone/comment_2_e5babe3bc4535946ee04e8339403fda4._comment
new file mode 100644
index 0000000000..084df436a2
--- /dev/null
+++ b/doc/special_remotes/rclone/comment_2_e5babe3bc4535946ee04e8339403fda4._comment
@@ -0,0 +1,19 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""Re: Need for git-annex-remote-rclone"""
+ date="2024-03-22T15:01:43Z"
+ content="""
+That's right, I have actually thought before that enough people use it that
+it would make sense to either build it in as haskell or ship the program
+with git-annex in a kind of contrib.
+
+With that said, there is also something to be said for distributing
+maintenance, and I think I'd at least want a committment to maintain it if
+it were added to git-annex, since git-annex-remote-rclone already has
+ongoing maintenance.
+
+Another angle is [[forum/New_external_special_remote_for_rclone]] which
+might see the special remote built into rclone itself, and so able to take
+advantage of rclone's internal API. That might supplant the shell script if
+it turns out to be better.
+"""]]

comment
diff --git a/doc/bugs/test__58___posix__95__spawnp_broken_10_on_darwin/comment_1_5741fabaa561b81989cc176e9acb4a99._comment b/doc/bugs/test__58___posix__95__spawnp_broken_10_on_darwin/comment_1_5741fabaa561b81989cc176e9acb4a99._comment
new file mode 100644
index 0000000000..ecf25cd9c3
--- /dev/null
+++ b/doc/bugs/test__58___posix__95__spawnp_broken_10_on_darwin/comment_1_5741fabaa561b81989cc176e9acb4a99._comment
@@ -0,0 +1,30 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 1"""
+ date="2024-03-22T14:47:02Z"
+ content="""
+This error message seems to indicate that git-annex tried to run a program
+named "security", which does not exist.
+
+I can reproduce the error message like so:
+
+	ghci> createProcess (proc "security" [])
+	*** Exception: security: createProcess: posix_spawnp: does not exist (No such file or directory)
+
+Since git-annex definitely does not normally try to run a program named
+"security" -- it doesn't even contain any String with that value -- I am
+puzzled by what could be causing it to do so. My best guess is something in
+your git configuration might be set. For example:
+
+	joey@darkstar:~/tmp/xx>git config annex.freezecontent-command security
+	joey@darkstar:~/tmp/xx>git-annex addurl file:///bin/sh
+	addurl file:///bin/sh
+	(to _bin_sh) sh: 1: security: not found
+
+Which well, rules out that particular git config, but there are several git
+configs that specify a command to run, so some other one.
+
+However, the test suite also runs with `GIT_CONFIG_NOSYSTEM` set, so git won't read
+the system gitconfig file, and it also sets HOME to a temp directory to avoid reading
+`~/.gitconfig`. So normally even such a git config setting should not affect the test suite.
+"""]]

diff --git a/doc/bugs/test__58___posix__95__spawnp_broken_10_on_darwin.mdwn b/doc/bugs/test__58___posix__95__spawnp_broken_10_on_darwin.mdwn
new file mode 100644
index 0000000000..81fef53a53
--- /dev/null
+++ b/doc/bugs/test__58___posix__95__spawnp_broken_10_on_darwin.mdwn
@@ -0,0 +1,25 @@
+### Please describe the problem.
+
+We observer error in our CI:
+
+```
+      ./Test/Framework.hs:86:
+      addurl on file:///private/tmp/nix-build-git-annex-10.20231129.drv-0/git-annex-10.20231129-src/.t/25/tmprepo0/myurl failed with unexpected exit code (transcript follows)
+      addurl file:///private/tmp/nix-build-git-annex-10.20231129.drv-0/git-annex-10.20231129-src/.t/25/tmprepo0/myurl 
+      git-annex: security: createProcess: posix_spawnp: does not exist (No such file or directory)
+      failed
+      addurl: 1 failed
+```
+
+https://github.com/cachix/pre-commit-hooks.nix/actions/runs/8355807260/job/22871621632
+
+### What steps will reproduce the problem?
+
+build git-annex and run tests on darwin
+
+### What version of git-annex are you using? On what operating system?
+
+10.20231129
+
+
+

policy on AI generated content
diff --git a/doc/contribute.mdwn b/doc/contribute.mdwn
index a005e22e81..54bf3b8f1c 100644
--- a/doc/contribute.mdwn
+++ b/doc/contribute.mdwn
@@ -61,6 +61,9 @@ or work on porting libraries needed by the Windows port.
 To send patches, either include the patch in a [[bug|bugs]] report (small
 patch) or put up a branch in a git repository containing your changes.
 
+[Policy on AI generated content](https://joeyh.name/blog/entry/policy_on_adding_AI_generated_content_to_my_software_projects/)
+(Summary: It's welcome! But see the page for important details.)
+
 ## learning some Haskell
 
 Want to learn some Haskell to get hacking on git-annex?

update
diff --git a/doc/design/balanced_preferred_content.mdwn b/doc/design/balanced_preferred_content.mdwn
index 2a84b1273c..0e2bc5d417 100644
--- a/doc/design/balanced_preferred_content.mdwn
+++ b/doc/design/balanced_preferred_content.mdwn
@@ -1,4 +1,4 @@
-Say we have 2 backup drives and want to fill them both evenly with files,
+Say we have 2 drives and want to fill them both evenly with files,
 different files in each drive. Currently, preferred content cannot express
 that entirely:
 
@@ -6,11 +6,20 @@ that entirely:
 * Or, can let both repos take whatever files, perhaps at random, that the
   other repo is not know to contain, but then repos will race and both get
   the same file, or similarly if they are not communicating frequently.
+  Existing preferred content expressions such as the one for archive group
+  have this problem.
 
 So, let's add a new expression: `balanced(group)`
 
+## implementation
+
 This would work by taking the list of uuids of all repositories in the
-group, and sorting them, which yields a list from 0..M-1 repositories.
+group that have enough free space to store a key, and sorting them, 
+which yields a list from 0..M-1 repositories.
+
+(To know if a repo has enough free space to store a key 
+will need [[todo/track_free_space_in_repos_via_git-annex_branch]]
+to be implemented.)
 
 To decide which repository wants key K, convert K to a number N in some
 stable way and then `N mod M` yields the number of the repository that
@@ -19,60 +28,29 @@ wants it, while all the rest don't.
 (Since git-annex keys can be pretty long and not all of them are random
 hashes, let's md5sum the key and then use the md5 as a number.)
 
-This expression is stable as long as the members of the group don't change.
-I think that's stable enough to work as a preferred content expression.
+## stability
+
+Note that this preferred content expression will not be stable. A change in 
+the members of the group will change which repository is selected. And
+changes in how full repositories are will also change which repo is
+selected.
 
-Now, you may want to be able to add a third repo and have the data be
-rebalanced, with some moving to it. And that would happen. However, as this
-scheme stands, it's equally likely that adding repo3 will make repo1 and
-repo2 want to swap files between them. So, we'll want to add some
-precautions to avoid a lot of data moving around in this case:
+Without stability, when another repo is added to the group, all data will
+be rebalanced, with some moving to it. Which could be desirable in some
+situations, but the problem is that it's likely that adding repo3 will make
+repo1 and repo2 want to swap some files between them,
+
+So, we'll want to add some precautions to avoid a lot of data moving around
+in such a case:
 
 	((balanced(backup) and not (copies=backup:1)) or present
 
 So once file lands on a backup drive, it stays there, even if more backup
 drives change the balancing.
 
------
-
-Some limitations:
-
-* The item size is not taken into account. One repo could end up with a
-  much larger item or items and so fill up faster. And the other repo
-  wouldn't then notice it was full and take up some slack.
-* With the complicated expression above, adding a new repo when one 
-  is full would not necessarily result in new files going to one of the 2
-  repos that still have space. Some items would end up going to the full
-  repo.
-
-These can be dealt with by noticing when a repo is full and moving some
-of it's files (any will do) to other repos in its group. I don't see a way
-to make preferred content express that movement though; it would need to be
-a manual/scripted process.
-
-> Could the size of each repo be recorded (either actual disk size or
-> desired max size) and when a repo is too full to hold an object, be left
-> out of the set of repos used to calculate where to store that object?
->
-> With the preferred content expression above with "present" in it, 
-> a repo being full would not cause any content to be moved off of it,
-> only new content that had not yet reached any of the repos in the 
-> group would be affected. That seems good.
-> 
-> This would need only a single one-time write to the git-annex branch,
-> to record the repo size. Then update a local counter for each repository
-> from the git-annex branch location log changes. 
-> There is a todo about doing this,
-> [[todo/track_free_space_in_repos_via_git-annex_branch]].
-> 
-> Of course, in the time after the git-annex branch was updated and before
-> it reaches the local repo, a repo can be full without us knowing about
-> it. Stores to it would fail, and perhaps be retried, until the updated
-> git-annex branch was synced.
-
------
-
-What if we have 5 backup repos and want each file to land in 3 of them?
+## use case: 3 of 5
+
+What if we have 5 backup repos and want each key to be stored in 3 of them?
 There's a simple change that can support that:
 `balanced(group:3)`
 
@@ -80,29 +58,15 @@ This works the same as before, but rather than just `N mod M`, take
 `N+I mod M` where I is [0..2] to get the list of 3 repositories that want a
 key.
 
-This does not really avoid the limitations above, but having more repos
-that want each file will reduce the chances that no repo will be able to
-take a given file. In the [[iabackup]] scenario, new clients will just be
-assigned until all the files reach the desired level or replication.
+However, once 3 of those 5 repos get full, new keys will only be able to be
+stored on 2 of them. At that point one or more new repos will need to be
+added to reach the goal of each key being stored in 3 of them. It would be
+possible to rebalance the 3 full repos by moving some keys from them to the
+other 2 repos, and eke out more storage before needing to add new
+repositories. A separate rebalancing pass, that does not use preferred
+content alone, could be implemented to handle this (see below).
 
-However.. Imagine there are 9 repos, all full, and some files have not
-reached desired level of replication. Seems like adding 1 more repo will make
-only 3 in 10 files be wanted by that new repo. Even if the repo has space
-for all the files, it won't be sufficient, and more repos would need to be
-added.
-
-One way to avoid this problem would be if the preferred content was only
-used for the initial distribution of files to a repo. If the repo has
-gotten all the files it wants, it could make a second pass and
-opportunistically get files it doesn't want but that it has space for
-and that don't have enough copies yet.
-Although this gets back to the original problem of multiple repos racing
-downloads and files getting more than the desired number of copies.
-
-> With the above idea of tracking when repos are full, the new repo
-> would want all files when the other 9 repos are full.
-
-----
+## use case: geographically distinct datacenters
 
 Of course this is not limited to backup drives. A more complicated example:
 There are 4 geographically distributed datacenters, each of which has some
@@ -112,7 +76,7 @@ on some drive there.
 This can be implemented by making a group for each datacenter, which all of
 its drives are in, and using `balanced()` to pick the drive that holds the
 copy of the file. The preferred content expression would be eg:
-	
+
     ((balanced(datacenterA) and not (copies=datacenterA:1)) or present
 
 In such a situation, to avoid a `N^2` remote interconnect, there might be a
@@ -126,20 +90,60 @@ the place that `balanced()` picks for a group. Eg,
 `balancedgroup=datacenterA` for 1 copy and `balancedgroup=group:datacenterA:2`
 for N copies.
 
-----
+The [[design/passthrough_proxy]] idea is an alternate way to put a
+repository in front of such a cluster, that does not need additional
+extensions to preferred content.
+
+## split brain situations
+
+Of course, in the time after the git-annex branch was updated and before
+it reaches the local repo, a repo can be full without us knowing about
+it. Stores to it would fail, and perhaps be retried, until the updated
+git-annex branch was synced. 
+
+In the worst case, a split brain situation
+can make the balanced preferred content expression
+pick a different repository to hold two independent
+stores of the same key. Eg, when one side thinks one repo is full,
+and the other side thinks the other repo is full.
+
+If `present` is used in the preferred content, both of them will then
+want to contain it. (Is `present` really needed like shown in the examples
+above?)
+
+If it's not, one of them will drop it and the other will
+usually maintain its copy. It would perhaps be possible for both of
+them to drop it, leading to a re-upload cycle. This needs some research
+to see if it's a real problem. 
+See [[todo/proving_preferred_content_behavior]].
+
+## rebalancing
+
+In both the 3 of 5 use case and a split brain situation, it's possible for
+content to end up not optimally balanced between repositories. git-annex
+can be made to operate in a mode where it does additional work to rebalance
+repositories. 
+
+This can be an option like --rebalance, that changes how the preferred content
+expression is evaluated. The user can choose where and when to run that.
+Eg, it might be run on a node inside a cluster after adding more storage to
+the cluster.
+

(Diff truncated)
Added a comment: Need for git-annex-remote-rclone
diff --git a/doc/special_remotes/rclone/comment_1_3fa761ad2e7a87e160b6d0e86814801c._comment b/doc/special_remotes/rclone/comment_1_3fa761ad2e7a87e160b6d0e86814801c._comment
new file mode 100644
index 0000000000..c698e2154a
--- /dev/null
+++ b/doc/special_remotes/rclone/comment_1_3fa761ad2e7a87e160b6d0e86814801c._comment
@@ -0,0 +1,10 @@
+[[!comment format=mdwn
+ username="oadams"
+ avatar="http://cdn.libravatar.org/avatar/ac166a5f89f10c4108e5150015e6751b"
+ subject="Need for git-annex-remote-rclone"
+ date="2024-03-14T01:03:46Z"
+ content="""
+In order to use rclone as a special remote, the user needs to download a separate Bash scriptfrom https://github.com/DanielDent/git-annex-remote-rclone and put it in their PATH. Since that extra dependency is only a few hundred lines of Bash, I would be interested in attempting to implement `Remote/Rclone.hs` so that the rclone special remote is entirely built into git-annex. However, I wanted to run it by you before more seriously considering investing time in doing that. What are your thoughts on this? I'm assuming the only reason rclone support isn't built into git-annex is just a lack of time and incentive, rather than a more fundamental technical reason. Is that right?
+
+Thanks for all your work on this tool. 
+"""]]

update
diff --git a/doc/design/passthrough_proxy.mdwn b/doc/design/passthrough_proxy.mdwn
index c5c9385168..91830f41ce 100644
--- a/doc/design/passthrough_proxy.mdwn
+++ b/doc/design/passthrough_proxy.mdwn
@@ -117,8 +117,11 @@ the client seems like the best choice by far.
 
 But, git-annex's git remotes don't currently ever do encryption. And
 special remotes don't communicate via the P2P protocol with a git remote.
-So none of git-annex existing remote implementations would be able to handle
-this case. So something will need to be changed in the remote
-implementation to handle this case.
+So none of git-annex's existing remote implementations would be able to handle
+this case. Something will need to be changed in the remote
+implementation for this.
 
 (Chunking has the same problem.)
+
+There's potentially a layering problem here, because exactly how encryption
+(or chunking) works can vary depending on the type of special remote.

update
diff --git a/doc/design/balanced_preferred_content.mdwn b/doc/design/balanced_preferred_content.mdwn
index f46eac8470..2a84b1273c 100644
--- a/doc/design/balanced_preferred_content.mdwn
+++ b/doc/design/balanced_preferred_content.mdwn
@@ -140,4 +140,6 @@ would surely have been in vain.)
 
 ## see also
 
-[[todo/proving_preferred_content_behavior]]
+[[todo/proving_preferred_content_behavior]]  
+[[todo/passthrough_proxy]]  
+
diff --git a/doc/design/passthrough_proxy.mdwn b/doc/design/passthrough_proxy.mdwn
index 16b5dc7f0e..c5c9385168 100644
--- a/doc/design/passthrough_proxy.mdwn
+++ b/doc/design/passthrough_proxy.mdwn
@@ -107,3 +107,18 @@ content. Eg, analize what files are typically requested, and store another
 copy of those on the proxy. Perhaps prioritize storing smaller files, where
 latency tends to swamp transfer speed.
 
+## encryption
+
+When the proxy is in front of a special remote that uses encryption, where
+does the encryption happen? It could either happen on the client before
+sending to the proxy, or the proxy could do the encryption since it
+communicates with the special remote. For security, doing the encryption on
+the client seems like the best choice by far.
+
+But, git-annex's git remotes don't currently ever do encryption. And
+special remotes don't communicate via the P2P protocol with a git remote.
+So none of git-annex existing remote implementations would be able to handle
+this case. So something will need to be changed in the remote
+implementation to handle this case.
+
+(Chunking has the same problem.)

link
diff --git a/doc/design/balanced_preferred_content.mdwn b/doc/design/balanced_preferred_content.mdwn
index 9adf201bcd..f46eac8470 100644
--- a/doc/design/balanced_preferred_content.mdwn
+++ b/doc/design/balanced_preferred_content.mdwn
@@ -137,3 +137,7 @@ In a split brain situation, there would be sets of repos doing work toward
 different solutions. On merge it would make sense to calculate a new
 solution that takes that work into account as well as possible. (Some work
 would surely have been in vain.)
+
+## see also
+
+[[todo/proving_preferred_content_behavior]]

fix spelling
diff --git a/doc/design/passthrouh_proxy.mdwn b/doc/design/passthrough_proxy.mdwn
similarity index 100%
rename from doc/design/passthrouh_proxy.mdwn
rename to doc/design/passthrough_proxy.mdwn

todo
diff --git a/doc/todo/proving_preferred_content_behavior.mdwn b/doc/todo/proving_preferred_content_behavior.mdwn
new file mode 100644
index 0000000000..73f181bc0d
--- /dev/null
+++ b/doc/todo/proving_preferred_content_behavior.mdwn
@@ -0,0 +1,75 @@
+Preferred content expressions can be complicated to write and reason about.
+A complex expression can involve lots of repositories that can get into
+different states, and needs to be written to avoid unwanted behavior.
+
+It would be very handy to provide some way to prove things about behavior
+of preferred content expressions, or a way to simulate the behavior of a
+network of git-annex repositories with a given preferred content configuration 
+
+## motivating examples
+
+For example, consider two reposities A and B. A is in group M and B is in
+group N. A has preferred content `not inallgroup=N` and B has `not inallgroup=M`.
+
+If A contains a file, then B will want to also get a copy. And things
+stabilize there. But if the file is removed from A, then B also wants to
+remove it. And once B has removed it, A wants a copy of it. And then B also
+wants a copy of it. So the result is that the file got transferred twice,
+to end up right back where we started.
+
+The worst case of this is `not present`, where the file gets dropped and
+transferred over and over again. The docs warn against using that one. But
+they can't warn about every bad preferred content expression.
+
+## balanced preferred content
+
+When [[design/balanced_preferred_content]] is added, a whole new level of
+complexity will exist in preferred content expressions, because now an
+expression does not make a file be wanted by a single repository, but
+shards the files amoung repositories in a group. 
+
+And up until this point preferred content expressions have behaved the same no
+matter the sizes of the underlying repositories, but balanced preferred
+content does take repository fullness into account, which further
+complicates fully understanding the behavior.
+
+Notice that `balanced()` (in the current design) is not stable when used
+on its own, and has to be used as part of a larger expression to make it
+stable, eg:
+
+    ((balanced(backup) and not (copies=backup:1)) or present
+
+So perhaps `balanced()` should include the other checks in it,
+to avoid the user shooting themselves in the foot. On the other 
+hand, if `balanced()` implicitly contains `present`, then `not balanced()`
+would include `not present`, which is bad!
+
+(For that matter, what does `not balanced()` even do currently?)
+
+## proof
+
+What could be proved about a preferred content expression?
+
+No idea really. Would be interesting to consider what formal methods can
+do here. Could a SAT solver be used somehow for example?
+
+## static analysis
+
+Clearly `not present` is an problematic preferred content expression. It
+would be good if git-annex warned and/or refused to set such an expression
+if it could detect it. Similarly `not groupwanted` could be detected as a
+problem when the group's preferred content expression contains `present`.
+
+Is there is a more general purpose and not expensive way to detect such
+problematic expressions, that can find problems such as the 
+`not inallgroup=N` example above?
+
+## simulation
+
+Simulation seems fairly straightforward, just simulate the network of
+git-annex repositories with random files with different sizes and
+metadata. Be sure to enforce invariants like numcopies the same as
+git-annex does.
+
+Since users can write preferred content expressions, this should be
+targeted at being used by end users.

update
diff --git a/doc/design/passthrouh_proxy.mdwn b/doc/design/passthrouh_proxy.mdwn
index 7860d0f21f..16b5dc7f0e 100644
--- a/doc/design/passthrouh_proxy.mdwn
+++ b/doc/design/passthrouh_proxy.mdwn
@@ -34,15 +34,15 @@ A proxy would not hold the content of files itself. It would be a clone of
 the git repository though, probably. Uploads and downloads would stream
 through the proxy. The git-annex [[P2P_protocol]] could be relayed in this way. 
 
-## discovering UUIDS
+## UUID discovery
 
-A significant difficulty in implementing a proxy for the P2P protocol is
-that each git-annex remote has a single UUID. But the remote that points at
-the proxy can't just have the UUID of the proxy's repository, git-annex
-needs to know that the remote can be used to access repositories with every
-UUID in the cluster.
+A significant difficulty in implementing a proxy is that each git-annex
+remote has a single UUID. But the remote that points at the proxy can't
+just have the UUID of the proxy's repository, git-annex needs to know that
+the proxy's remote can be used to access repositories with every UUID in
+the cluster.
 
-----
+### UUID discovery via P2P protocol extension
 
 Could the P2P protocol be extended to let the proxy communicate the UUIDs
 of all the repositories behind it?
@@ -57,7 +57,7 @@ a proxy. Then it could do UUID discovery each time git-annex starts up.
 But that adds significant overhead, git-annex would be making a connection
 to the proxy in situations where it is not going to use it.
 
-----
+### UUID discovery via git-annex branch
 
 Could the proxy's set of UUIDs instead be recorded somewhere in the
 git-annex branch?

update
diff --git a/doc/design/passthrouh_proxy.mdwn b/doc/design/passthrouh_proxy.mdwn
index 6b5063e7df..7860d0f21f 100644
--- a/doc/design/passthrouh_proxy.mdwn
+++ b/doc/design/passthrouh_proxy.mdwn
@@ -88,3 +88,22 @@ The remote interface operates on object files stored on disk. See
 [[todo/transitive_transfers]] for discussion of that problem. If proxies
 get implemented, that problem should be revisited.
 
+## speed
+
+A passthrough proxy should be as fast as possible so as not to add overhead
+to a file retrieve, store, or checkpresent. This probably means that
+it keeps TCP connections open to each host in the cluster. It might use a
+protocol with less overhead than ssh.
+
+In the case of checkpresent, it would be possible for the proxy to not
+communicate with the cluster to check that the data is still present on it.
+As long as all access is intermediated via the proxy, its git-annex branch
+could be relied on to always be correct, in theory. Proving that theory,
+making sure to account for all possible race conditions and other scenarios,
+would be necessary for such an optimisation.
+
+Another way the proxy could speed things up is to cache some subset of
+content. Eg, analize what files are typically requested, and store another
+copy of those on the proxy. Perhaps prioritize storing smaller files, where
+latency tends to swamp transfer speed.
+

todo
diff --git a/doc/design/passthrouh_proxy.mdwn b/doc/design/passthrouh_proxy.mdwn
new file mode 100644
index 0000000000..6b5063e7df
--- /dev/null
+++ b/doc/design/passthrouh_proxy.mdwn
@@ -0,0 +1,90 @@
+When [[balanced_preferred_content]] is used, there may be many repositories
+in a location -- either a server or a cluster -- and getting any given file
+may need to access any of them. Configuring remotes for each repository
+adds a lot of complexity, both in setting up access controls on each
+server, and for the user. 
+
+Particularly on the user side, when ssh is used they may have to deal with
+many different ssh host keys, as well as adding new remotes or removing
+existing remotes to keep up with changes are made on the server side.
+
+A proxy would avoid this complexity. It also allows limiting network
+ingress to a single point.
+
+Ideally a proxy would look like any other git-annex remote. All the files
+stored anywhere in the cluster would be available to retrieve from the
+proxy. When a file is sent to the proxy, it would store it somewhere in the
+cluster.
+
+Currently the closest git-annex can get to implementing such a proxy is a
+transfer repository that wants all content that is not yet stored in the
+cluster. This allows incoming transfers to be accepted and distributed to
+nodes of the cluster. To get data back out of the cluster, there has to be
+some communication that it is preferred content (eg, setting metadata),
+then after some delay for it to be copied back to the transfer repository,
+it becomes available for the client to download it. And once it knows the
+client has its copy, it can be removed from the transfer repository.
+
+That is quite slow, and rather clumsy. And it risks the transfer repository
+filling up with data that has been requested by clients that have not yet
+picked it up, or with incoming transfers that have not yet reached the
+cluster.
+
+A proxy would not hold the content of files itself. It would be a clone of
+the git repository though, probably. Uploads and downloads would stream
+through the proxy. The git-annex [[P2P_protocol]] could be relayed in this way. 
+
+## discovering UUIDS
+
+A significant difficulty in implementing a proxy for the P2P protocol is
+that each git-annex remote has a single UUID. But the remote that points at
+the proxy can't just have the UUID of the proxy's repository, git-annex
+needs to know that the remote can be used to access repositories with every
+UUID in the cluster.
+
+----
+
+Could the P2P protocol be extended to let the proxy communicate the UUIDs
+of all the repositories behind it?
+
+Once the client git-annex knows the set of UUIDs behind the proxy, it can
+instantiate a remote object per uuid, each of which accesses the proxy, but
+with a different UUID.
+
+But, git-annx usually only does UUID discovery the first time a ssh remote
+is accessed. So it would need to discover at that point that the remote is
+a proxy. Then it could do UUID discovery each time git-annex starts up.
+But that adds significant overhead, git-annex would be making a connection
+to the proxy in situations where it is not going to use it.
+
+----
+
+Could the proxy's set of UUIDs instead be recorded somewhere in the
+git-annex branch?
+
+With this approach, git-annex would know as soon as it sees the proxy's
+UUID that this is a proxy for this other set of UUIDS. (Unless its
+git-annex branch is not up-to-date.) And then it can instantiate a UUID for
+each remote.
+
+One difficulty with this is that, when the git-annex branch is not up to
+date with changes from the proxy, git-annex may try to access repositories
+that are no longer available behind the proxy. That failure would be
+handled the same as any other currently unavailable repository. Also
+git-annex would not use the full set of repositories, so might not be able
+to store data when eg, all the repositories that is knows about are full.
+Just getting the git-annex back in sync should recover from either
+situation.
+
+## streaming to special remotes
+
+As well as being an intermediary to git-annex repositories, the proxy could
+provide access to other special remotes. That could be an object store like
+S3, which might be internal to the cluster or not. When using a cloud
+service like S3, only the proxy needs to know the access credentials.
+
+Currently git-annex does not support streaming content to special remotes.
+The remote interface operates on object files stored on disk. See
+[[todo/transitive_transfers]] for discussion of that problem. If proxies
+get implemented, that problem should be revisited.
+

thoughts
diff --git a/doc/design/balanced_preferred_content.mdwn b/doc/design/balanced_preferred_content.mdwn
index 0acc3562e3..9adf201bcd 100644
--- a/doc/design/balanced_preferred_content.mdwn
+++ b/doc/design/balanced_preferred_content.mdwn
@@ -104,6 +104,30 @@ downloads and files getting more than the desired number of copies.
 
 ----
 
+Of course this is not limited to backup drives. A more complicated example:
+There are 4 geographically distributed datacenters, each of which has some
+number of drives. Each file should have 1 copy stored in each datacenter,
+on some drive there. 
+
+This can be implemented by making a group for each datacenter, which all of
+its drives are in, and using `balanced()` to pick the drive that holds the
+copy of the file. The preferred content expression would be eg:
+	
+    ((balanced(datacenterA) and not (copies=datacenterA:1)) or present
+
+In such a situation, to avoid a `N^2` remote interconnect, there might be a
+transfer repository in each datacenter, that is in front of its drives. The
+transfer repository should want files that have not yet reached the
+destination drive. How to write a preferred content expression for that?
+It might be sufficient to use `copies=datacenterA:1`, so long as the file
+reaching any drive in the datacenter is enough. But may want to add
+something analagous to `inallgroup=` that checks if a file is in
+the place that `balanced()` picks for a group. Eg, 
+`balancedgroup=datacenterA` for 1 copy and `balancedgroup=group:datacenterA:2`
+for N copies.
+
+----
+
 Another possibility to think about is to have one repo calculate which
 files to store on which repos, to best distribute and pack them. The first
 repo that writes a solution would win and other nodes would work to move

diff --git a/doc/bugs/Get_crashes_when_remote_contains_non-english_chars.mdwn b/doc/bugs/Get_crashes_when_remote_contains_non-english_chars.mdwn
new file mode 100644
index 0000000000..fe6098f754
--- /dev/null
+++ b/doc/bugs/Get_crashes_when_remote_contains_non-english_chars.mdwn
@@ -0,0 +1,107 @@
+Hi,
+
+### Please describe the problem.
+I'm trying to set up a git-annex repo for my books/technical papers to have easy access to them on my desktop and laptop. I'm using a centralized server (following [this guide](https://git-annex.branchable.com/tips/centralized_git_repository_tutorial/on_your_own_server/)) to make it easy to sync between my machines.
+
+The issue is however that sqlite crashes when I'm trying to get a file from my server. See the log further down for the error message. I'm suspecting it is due to the repo on my server is named `Böcker` (swedish name for books). It does work if I'm cloning it locally on my server. E.g.
+
+[[!format sh """
+$ ssh server
+$ git clone /mnt/Valhalla/Böcker books
+$ cd books
+$ git annex init
+init  ok
+(recording state in git...)
+$ git annex get facklitteratur/rapporter/143-rods.pdf
+get facklitteratur/rapporter/143-rods.pdf (from origin...) 
+ok                                
+(recording state in git...)
+"""]]
+
+And if I use the `ssh://server/~/books` as a remote it works fine.
+
+### What steps will reproduce the problem?
+[[!format sh """
+$ ssh server
+server$ mkdir /tmp/Böcker
+server$ cd /tmp/Böcker
+server$ git init
+server$ git annex init
+server$ dd if=/dev/zeo of=foo bs=4k count=1
+server$ git annex add foo
+server$ git commit -m "Add foo"
+server$ exit
+$ git clone ssh://server/tmp/Böcker /tmp/Böcker
+$ cd Böcker
+$ git annex init
+$ git annex get foo
+"""]]
+Note that `foo` need to have some size hence the use of `dd`, just doing `touch foo` did not trigger the issue for me.
+
+### What version of git-annex are you using? On what operating system?
+On my desktop:
+[[!format sh """
+$ git annex version
+git-annex version: 10.20240227
+build flags: Assistant Webapp Pairing Inotify DBus DesktopNotify TorrentParser MagicMime Feeds Testsuite S3 WebDAV
+dependency versions: aws-0.23 bloomfilter-2.0.1.2 cryptonite-0.30 DAV-1.3.4 feed-1.3.2.1 ghc-9.2.5 http-client-0.7.13.1 persistent-sqlite-2.13.1.0 torrent-10000.1.1 uuid-1.3.15 yesod-1.6.2.1
+key/value backends: SHA256E SHA256 SHA512E SHA512 SHA224E SHA224 SHA384E SHA384 SHA3_256E SHA3_256 SHA3_512E SHA3_512 SHA3_224E SHA3_224 SHA3_384E SHA3_384 SKEIN256E SKEIN256 SKEIN512E SKEIN512 BLAKE2B256E BLAKE2B256 BLAKE2B512E BLAKE2B512 BLAKE2B160E BLAKE2B160 BLAKE2B224E BLAKE2B224 BLAKE2B384E BLAKE2B384 BLAKE2BP512E BLAKE2BP512 BLAKE2S256E BLAKE2S256 BLAKE2S160E BLAKE2S160 BLAKE2S224E BLAKE2S224 BLAKE2SP256E BLAKE2SP256 BLAKE2SP224E BLAKE2SP224 SHA1E SHA1 MD5E MD5 WORM URL X*
+remote types: git gcrypt p2p S3 bup directory rsync web bittorrent webdav adb tahoe glacier ddar git-lfs httpalso borg hook external
+operating system: linux x86_64
+supported repository versions: 8 9 10
+upgrade supported from repository versions: 0 1 2 3 4 5 6 7 8 9 10
+"""]]
+
+Running Guix with linux kernel 6.6.18
+
+On my server:
+[[!format sh """
+$ git annex version
+git-annex version: 10.20240227-gbee3abab14f99f3e3d981d8255ca0dd4ff124a84
+build flags: Assistant Webapp Pairing Inotify DBus DesktopNotify TorrentParser MagicMime Benchmark Feeds Testsuite S3 WebDAV
+dependency versions: aws-0.24 bloomfilter-2.0.1.2 crypton-0.34 DAV-1.3.4 feed-1.3.2.1 ghc-9.2.8 http-client-0.7.15 persistent-sqlite-2.13.1.0 torrent-10000.1.3 uuid-1.3.15 yesod-1.6.2.1
+key/value backends: SHA256E SHA256 SHA512E SHA512 SHA224E SHA224 SHA384E SHA384 SHA3_256E SHA3_256 SHA3_512E SHA3_512 SHA3_224E SHA3_224 SHA3_384E SHA3_384 SKEIN256E SKEIN256 SKEIN512E SKEIN512 BLAKE2B256E BLAKE2B256 BLAKE2B512E BLAKE2B512 BLAKE2B160E BLAKE2B160 BLAKE2B224E BLAKE2B224 BLAKE2B384E BLAKE2B384 BLAKE2BP512E BLAKE2BP512 BLAKE2S256E BLAKE2S256 BLAKE2S160E BLAKE2S160 BLAKE2S224E BLAKE2S224 BLAKE2SP256E BLAKE2SP256 BLAKE2SP224E BLAKE2SP224 SHA1E SHA1 MD5E MD5 WORM URL X*
+remote types: git gcrypt p2p S3 bup directory rsync web bittorrent webdav adb tahoe glacier ddar git-lfs httpalso borg hook external
+operating system: linux x86_64
+supported repository versions: 8 9 10
+upgrade supported from repository versions: 0 1 2 3 4 5 6 7 8 9 10
+local repository version: 10
+"""]]
+Running Arch Linux with kernel 6.7.9-arch1-1
+
+### Please provide any additional information below.
+Here is the error message I get without any changes to the config (I can silence the warning by enabling annex.sshcaching).
+[[!format sh """
+⎣plattfot@desktop Böcker⎦ git annex get facklitteratur/rapporter/143-rods.pdf
+get facklitteratur/rapporter/143-rods.pdf (from origin...) 
+
+  You have enabled concurrency, but git-annex is not able to use ssh connection caching. This may result in multiple ssh processes prompting for passwords at the same time.
+
+  annex.sshcaching is not set to true
+git-annex: sqlite worker thread crashed: SQLite3 returned ErrorCan'tOpen while attempting to perform open "/mnt/Valhalla/B\65533\65533cker/.git/annex/keysdb/db".
+p2pstdio: 1 failed
+
+  Lost connection (fd:19: hGetChar: end of file)
+
+  Transfer failed
+
+  Unable to access these remotes: origin
+
+  Maybe add some of these git remotes (git remote add ...):
+  	8c92de49-1a89-4870-8186-b0099ad84be6 -- plattfot@server:~/books
+failed
+get: 1 failed
+
+# End of transcript or log.
+"""]]
+
+Note that this was run after I tested to create a repo with the name books.
+
+### Have you had any luck using git-annex before? (Sometimes we get tired of reading bug reports all day and a lil' positive end note does wonders)
+
+Sadly no, I manage to hit this on my first try of git-annex.  But I have known about git-annex for a few years, never found a good use for it in my workflow. As I've been using Syncthing for bigger files and that has been working ok.  But with that you pretty much get all or nothing and conflicts are no fun to resolve.  With my books I want to have my .bib file in git but somehow sync the files. I did not want to put everything in a normal git repo as the books directory takes about 64GB. After looking around I remembered git-annex and reading the walkthrough it seems to be a perfect fit! The killer feature in my opinion is that I can specify what files to sync. Which will be handy on my laptop as I don't really want to use up 64GB just so I can read one or two papers.
+
+I'm not giving up on this that easily. Worst case I'll just rename my repo on my server to Books.
+
+Thank you for all the hours developing this software!
+

comment
diff --git a/doc/bugs/git-annex_is_slow_at_reading_file_content/comment_13_0eb12582a3e182b697a07b833cfbe384._comment b/doc/bugs/git-annex_is_slow_at_reading_file_content/comment_13_0eb12582a3e182b697a07b833cfbe384._comment
new file mode 100644
index 0000000000..91690fe040
--- /dev/null
+++ b/doc/bugs/git-annex_is_slow_at_reading_file_content/comment_13_0eb12582a3e182b697a07b833cfbe384._comment
@@ -0,0 +1,13 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 13"""
+ date="2024-03-11T13:43:19Z"
+ content="""
+<https://hackage.haskell.org/package/botan-low> is another possibility.
+There is a significant effort ongoing to build up this library stack in
+haskell. It does need the botan C library to be installed separately
+unfortunately (I've suggested they embed it).
+
+I have not benchmarked it yet but the docs say it supports hardware
+accellerated SHA1, SHA2, SHA3 on x86 and also SHA1, SHA2 on arm64.
+"""]]

update
diff --git a/Database/ContentIdentifier.hs b/Database/ContentIdentifier.hs
index d40bf4faf5..bbf67dcfb1 100644
--- a/Database/ContentIdentifier.hs
+++ b/Database/ContentIdentifier.hs
@@ -77,7 +77,7 @@ databaseIsEmpty (ContentIdentifierHandle _ b) = b
 -- ContentIndentifiersKeyRemoteCidIndex speeds up queries like 
 -- getContentIdentifiers, but it is not used for
 -- getContentIdentifierKeys. ContentIndentifiersCidRemoteKeyIndex was
--- added to speed that up.
+-- addedto speed that up.
 share [mkPersist sqlSettings, mkMigrate "migrateContentIdentifier"] [persistLowerCase|
 ContentIdentifiers
   remote UUID
diff --git a/doc/todo/track_free_space_in_repos_via_git-annex_branch.mdwn b/doc/todo/track_free_space_in_repos_via_git-annex_branch.mdwn
index a50c698eb0..3b0acecbcc 100644
--- a/doc/todo/track_free_space_in_repos_via_git-annex_branch.mdwn
+++ b/doc/todo/track_free_space_in_repos_via_git-annex_branch.mdwn
@@ -78,3 +78,11 @@ Note that the use of `git cat-file` in union merge is not --buffer
 streaming, so is slower than the patch parsing method that was discussed in
 the previous section. So it might be possible to speed up git-annex branch
 merging using patch parsing.
+
+Note that Database.ContentIdentifier and Database.ImportFeed also update
+by diffing from the old to new git-annex branch (with `git cat-file` to
+read log files) so could also be sped up by being done at git-annex branch
+merge time. Those are less expensive than diffing the location logs only
+because the logs they diff are less often used, and the work is only 
+done when relevant commands are run.
+

update
diff --git a/doc/design/balanced_preferred_content.mdwn b/doc/design/balanced_preferred_content.mdwn
index 9a3f9badbd..0acc3562e3 100644
--- a/doc/design/balanced_preferred_content.mdwn
+++ b/doc/design/balanced_preferred_content.mdwn
@@ -7,7 +7,7 @@ that entirely:
   other repo is not know to contain, but then repos will race and both get
   the same file, or similarly if they are not communicating frequently.
 
-So, let's add a new expression: `balanced_amoung(group)`
+So, let's add a new expression: `balanced(group)`
 
 This would work by taking the list of uuids of all repositories in the
 group, and sorting them, which yields a list from 0..M-1 repositories.
@@ -28,7 +28,7 @@ scheme stands, it's equally likely that adding repo3 will make repo1 and
 repo2 want to swap files between them. So, we'll want to add some
 precautions to avoid a lot of data moving around in this case:
 
-	((balanced_amoung(backup) and not (copies=backup:1)) or present
+	((balanced(backup) and not (copies=backup:1)) or present
 
 So once file lands on a backup drive, it stays there, even if more backup
 drives change the balancing.
@@ -74,7 +74,7 @@ a manual/scripted process.
 
 What if we have 5 backup repos and want each file to land in 3 of them?
 There's a simple change that can support that:
-`balanced_amoung(group:3)`
+`balanced(group:3)`
 
 This works the same as before, but rather than just `N mod M`, take
 `N+I mod M` where I is [0..2] to get the list of 3 repositories that want a
diff --git a/doc/todo/track_free_space_in_repos_via_git-annex_branch.mdwn b/doc/todo/track_free_space_in_repos_via_git-annex_branch.mdwn
index 4cb82798ff..a50c698eb0 100644
--- a/doc/todo/track_free_space_in_repos_via_git-annex_branch.mdwn
+++ b/doc/todo/track_free_space_in_repos_via_git-annex_branch.mdwn
@@ -21,11 +21,7 @@ repos that had a maxsize recorded, essentially for free.
 
 But 8 seconds is rather a long time to block a `git-annex push`
 type command. Which would be needed if any remote's preferred content
-expression used `balanced_amoung`.
-
-It would help some to cache the calculated sizes in eq a sqlite db, update
-the cache after sending or dropping content, and invalidate the cache when
-git-annex branch update merges in a git-annex branch from elsewhere.
+expression used the free space information.
 
 Would it be possible to update incrementally from the previous git-annex
 branch to the current one? That's essentially what `git-annex log
@@ -39,13 +35,46 @@ particular git-annex branch commit. We don't care about sizes at
 intermediate points in time, which that command does calculate.
 
 See [[todo/info_--size-history]] for the subtleties that had to be handled.
-In particular, diffing from the previous git-annex branch commit to current may
+In particular, compating the previous git-annex branch commit to current may
 yield lines that seem to indicate content was added to a repo, but in fact
-that repo already had that content at the previous git-annex branch commit.
-So it seems it would have to look up the location log's value at the 
-previous commit, either querying the git-annex branch or cached state.
+that repo already had that content at the previous git-annex branch commit
+and another log line was recorded elsewhere redundantly.
+So it needs to look at the location log's value at the 
+previous commit in order to determine if a change to a log should be
+counted.
 
 Worst case, that's queries of the location log file for every single key.
 If queried from git, that would be slow -- slower than `git-annex info`'s
 streaming approach. If they were all cached in a sqlite database, it might
 manage to be faster?
+
+## incremental update via git diff
+
+Could `git diff -U1000000` be used and the patch parsed to get the complete
+old and new location log? (Assuming no log file ever reaches a million
+lines.) I tried this in my big repo, and even diffing from the first
+git-annex branch commit to the last took 7.54 seconds. 
+
+Compare that with the method used by `git-annex info`'s size gathering, of
+dumping out the content of all files on the branch with `git ls-tree -r
+git-annex |awk '{print $3}'|git cat-file --batch --buffer`, which only
+takes 3 seconds. So, this is not ideal when diffing to too old a point.
+
+Diffing in my big repo to the git-annex branch from 2020 takes 4 seconds.  
+... from 3 months ago takes 2 seconds.  
+... from 1 week ago takes 1 second.  
+
+## incremental update when merging git-annex branch
+
+When merging git-annex branch changes into .git/annex/index, 
+it already diffs between the branch and the index and uses `git cat-file`
+to get both versions of the file in order to union merge them.
+
+That's essentially the same information needed to do the incremental update
+of the repo sizes. So could update sizes at the same time as merging the
+git-annex branch. That would be essentially free!
+
+Note that the use of `git cat-file` in union merge is not --buffer
+streaming, so is slower than the patch parsing method that was discussed in
+the previous section. So it might be possible to speed up git-annex branch
+merging using patch parsing.

Added a comment: TLS v1.2 EMS (Extended Master/Main Secret)
diff --git a/doc/bugs/tls__58___peer_does_not_support_Extended_Main_Secret/comment_1_731db4542017bef831bc7a06b70f79fb._comment b/doc/bugs/tls__58___peer_does_not_support_Extended_Main_Secret/comment_1_731db4542017bef831bc7a06b70f79fb._comment
new file mode 100644
index 0000000000..88e84961b9
--- /dev/null
+++ b/doc/bugs/tls__58___peer_does_not_support_Extended_Main_Secret/comment_1_731db4542017bef831bc7a06b70f79fb._comment
@@ -0,0 +1,26 @@
+[[!comment format=mdwn
+ username="ewen"
+ avatar="http://cdn.libravatar.org/avatar/605b2981cb52b4af268455dee7a4f64e"
+ subject="TLS v1.2 EMS (Extended Master/Main Secret)"
+ date="2024-03-07T03:01:20Z"
+ content="""
+From some more research it seems that Extended Master Secret (aka Extended Main Secret) is a TLS 1.2 only extension, to work around a problem with TLS 1.2 (eg, [2015 post about the problem](https://www.tripwire.com/state-of-security/tls-extended-master-secret-extension-fixing-a-hole-in-tls)).
+
+TLS v1.3 doesn't have this problem, by design, AFAIK.  And thus clients/servers supporting TLS v1.3 entirely avoids the problem (possibly why I have only found it on a few servers; the one I looked into in detail definitely won't connect with TLS v1.3 right now, but they're looking into it).
+
+The webserver support can be confirmed with, eg forced TLS v1.2:
+
+```
+echo \"\" | openssl s_client -tls1_2 -connect WEBSERVER:443 2>&1 | egrep \"Protocol|Extended master\"
+```
+
+and forced TLS v1.3 to check if that will work:
+
+```
+echo \"\" | openssl s_client -tls1_3 -connect WEBSERVER:443
+```
+
+Hopefully that means the number of impacted sites is *relatively* small (eg, ones that haven't enabled TLS v1.3 support in the last 5+ years).
+
+Ewen
+"""]]

diff --git a/doc/bugs/tls__58___peer_does_not_support_Extended_Main_Secret.mdwn b/doc/bugs/tls__58___peer_does_not_support_Extended_Main_Secret.mdwn
new file mode 100644
index 0000000000..5723e07957
--- /dev/null
+++ b/doc/bugs/tls__58___peer_does_not_support_Extended_Main_Secret.mdwn
@@ -0,0 +1,101 @@
+### Please describe the problem.
+
+On the current [macOS `HomeBrew` build of the current git-annex](https://formulae.brew.sh/formula/git-annex) (10.20240227), it appears that the build dependencies have dragged in the [latest Haskell `tls` 
+package](https://hackage.haskell.org/package/tls-2.0.1/docs/Network-TLS.html).  Which now defaults `supportedExtendedMainSecret` to `RequireEMS` (previously it seems to have been `AllowEMS`; see eg [darcs bug report of similar error](https://bugs.darcs.net/issue2715)).
+
+The result of this is that some podcast feeds, from webservers which do not support EMS, fail with an error, eg:
+
+```
+importfeed https://risky.biz/feeds/risky-business
+  download failed: HandshakeFailed (Error_Protocol "peer does not support Extended Main Secret" HandshakeFailure)
+
+  warning: downloading the feed failed (feed: https://risky.biz/feeds/risky-business)
+ok
+```
+
+(And presumably this will also affect some non-podcast HTTPS downloads; I found it in a podcast download context.)
+
+I believe this "Extended Main Secret" is also known as "Extended Master Secret", aka [RFC 7627](https://www.ietf.org/rfc/rfc7627.html), which was written up in 2015.  So I can understand why ~9 years later the Haskell `tls` library is defaulting to insisting on EMS in a new major version.  Unfortunately not all webservers, especially podcast feed webservers, have caught up with this.
+
+As best I can tell git annex is getting this `tls` dependency via [`http-client`](https://hackage.haskell.org/package/http-client) which uses [`http-client-tls`](https://hackage.haskell.org/package/http-client-tls-0.3.6.3), and `http-client-tls` appears to just have a `tls (>=1.2)` dependency, which is presumably how `tls-2.0.0` / `tls-2.0.1` got dragged in, with these new defaults.
+
+I'm unclear if git-annex is in a position to pass `AllowEMS` to the TLS library (and thus restore to the old default).  But at least in the short term it might be worth considering doing that if possible.
+
+### What steps will reproduce the problem?
+
+Currently I have three podcast feeds (two from the same webserver) which fail:
+
+```
+git annex importfeed https://risky.biz/feeds/risky-business
+```
+
+```
+git annex importfeed https://risky.biz/feeds/risky-business-news
+```
+
+```
+git annex importfeed https://www.thecultureoftech.com/index.php/feed/podcast/ 
+```
+
+(Given the irony that the first two are are an InfoSec podcast, I have also reported this missing EMS extension support to them as well, so it may get fixed before you try it.)
+
+It looks like I've also had one media file download fail repeatedly for the same reason (but the podcast feed itself downloads okay):
+
+```
+git annex addurl https://traffic.omny.fm/d/clips/53b6fe2a-4ef6-4356-ae92-a61500df6da0/40b3f537-c161-4823-ae44-af3a007e121b/b2682900-b36c-447b-812d-b1290049fea8/audio.mp3
+```
+
+### What version of git-annex are you using? On what operating system?
+
+git annex 10.20240227, on macOS Ventura (13.6.3).  With git annex installed from HomeBrew.
+
+```
+ewen@basadi:~$ git annex version
+git-annex version: 10.20240227
+build flags: Assistant Webapp Pairing FsEvents TorrentParser MagicMime Benchmark Feeds Testsuite S3 WebDAV
+dependency versions: aws-0.24.1 bloomfilter-2.0.1.2 crypton-0.34 DAV-1.3.4 feed-1.3.2.1 ghc-9.6.3 http-client-0.7.16 persistent-sqlite-2.13.3.0 torrent-10000.1.3 uuid-1.3.15 yesod-1.6.2.1
+key/value backends: SHA256E SHA256 SHA512E SHA512 SHA224E SHA224 SHA384E SHA384 SHA3_256E SHA3_256 SHA3_512E SHA3_512 SHA3_224E SHA3_224 SHA3_384E SHA3_384 SKEIN256E SKEIN256 SKEIN512E SKEIN512 BLAKE2B256E BLAKE2B256 BLAKE2B512E BLAKE2B512 BLAKE2B160E BLAKE2B160 BLAKE2B224E BLAKE2B224 BLAKE2B384E BLAKE2B384 BLAKE2BP512E BLAKE2BP512 BLAKE2S256E BLAKE2S256 BLAKE2S160E BLAKE2S160 BLAKE2S224E BLAKE2S224 BLAKE2SP256E BLAKE2SP256 BLAKE2SP224E BLAKE2SP224 SHA1E SHA1 MD5E MD5 WORM URL X*
+remote types: git gcrypt p2p S3 bup directory rsync web bittorrent webdav adb tahoe glacier ddar git-lfs httpalso borg hook external
+operating system: darwin x86_64
+supported repository versions: 8 9 10
+upgrade supported from repository versions: 0 1 2 3 4 5 6 7 8 9 10
+ewen@basadi:~$ 
+```
+
+### Please provide any additional information below.
+
+[[!format sh """
+ewen@basadi:~/Music/podcasts$ git annex importfeed https://www.thecultureoftech.com/index.php/feed/podcast/ 
+importfeed gathering known urls ok
+importfeed https://www.thecultureoftech.com/index.php/feed/podcast/ 
+  download failed: HandshakeFailed (Error_Protocol "peer does not support Extended Main Secret" HandshakeFailure)
+
+  warning: downloading the feed failed (feed: https://www.thecultureoftech.com/index.php/feed/podcast/)
+ok
+ewen@basadi:~/Music/podcasts$ 
+ewen@basadi:~/Music/podcasts$ git annex addurl https://traffic.omny.fm/d/clips/53b6fe2a-4ef6-4356-ae92-a61500df6da0/40b3f537-c161-4823-ae44-af3a007e121b/b2682900-b36c-447b-812d-b1290049fea8/audio.mp3
+addurl https://traffic.omny.fm/d/clips/53b6fe2a-4ef6-4356-ae92-a61500df6da0/40b3f537-c161-4823-ae44-af3a007e121b/b2682900-b36c-447b-812d-b1290049fea8/audio.mp3 
+git-annex: HttpExceptionRequest Request {
+  host                 = "traffic.omny.fm"
+  port                 = 443
+  secure               = True
+  requestHeaders       = [("Accept-Encoding",""),("User-Agent","git-annex/10.20240227")]
+  path                 = "/d/clips/53b6fe2a-4ef6-4356-ae92-a61500df6da0/40b3f537-c161-4823-ae44-af3a007e121b/b2682900-b36c-447b-812d-b1290049fea8/audio.mp3"
+  queryString          = ""
+  method               = "HEAD"
+  proxy                = Nothing
+  rawBody              = False
+  redirectCount        = 10
+  responseTimeout      = ResponseTimeoutDefault
+  requestVersion       = HTTP/1.1
+  proxySecureMode      = ProxySecureWithConnect
+}
+ (InternalException (HandshakeFailed (Error_Protocol "peer does not support Extended Main Secret" HandshakeFailure)))
+failed
+addurl: 1 failed
+ewen@basadi:~/Music/podcasts$ 
+"""]]
+
+### Have you had any luck using git-annex before? (Sometimes we get tired of reading bug reports all day and a lil' positive end note does wonders)
+
+Absolutely, I've been using git-annex as my podcatcher (among other reasons) for about a decade at this point.  Thanks for developing it!

Added a comment
diff --git a/doc/forum/Possible_to_restore_from_an_encrypted_s3_remote__63__/comment_2_ddd6a56d583fd5b9d676365f4ebd9b68._comment b/doc/forum/Possible_to_restore_from_an_encrypted_s3_remote__63__/comment_2_ddd6a56d583fd5b9d676365f4ebd9b68._comment
new file mode 100644
index 0000000000..48c4394cfc
--- /dev/null
+++ b/doc/forum/Possible_to_restore_from_an_encrypted_s3_remote__63__/comment_2_ddd6a56d583fd5b9d676365f4ebd9b68._comment
@@ -0,0 +1,30 @@
+[[!comment format=mdwn
+ username="bbigras"
+ avatar="http://cdn.libravatar.org/avatar/f1c0201e3f1435eaab02c803a33c52ae"
+ subject="comment 2"
+ date="2024-03-06T17:03:50Z"
+ content="""
+I'm not sure I understand what you mean.
+
+Do you mean that I should clone the s3 bucket with a tool like git-remote-s3? I'm not sure how to do that.
+
+I tried downloading the whole bucket from backblaze, but since I enabled encryption on the bucket, backblaze doesn't let me download the encrypted files. I'm not sure yet if I can get those files using their api.
+
+Just in case I didn't explain correctly what I'm trying to do, I'm trying to do something like this, on a new computer with a fresh ~/Document:
+
+```bash
+❯ cd ~/Documents
+❯ git init
+Initialized empty Git repository in /home/bbigras/Documents/.git/
+❯ git annex init
+init  ok
+(recording state in git...)
+❯ git annex initremote backblaze type=S3 signature=v4 host=s3.us-west-000.backblazeb2.com bucket=my-bucket protocol=https encryption=hybrid keyid=my-key-id
+initremote backblaze (encryption setup) (to gpg keys: my-key-id) (checking bucket...)
+  The bucket already exists, and its annex-uuid file indicates it is used by a different special remote.
+
+git-annex: Cannot reuse this bucket.
+failed
+initremote: 1 failed
+```
+"""]]

Added a comment: Still experimental?
diff --git a/doc/tuning/comment_5_0143b798e75ad25c5917794a49a879fb._comment b/doc/tuning/comment_5_0143b798e75ad25c5917794a49a879fb._comment
new file mode 100644
index 0000000000..aee3de42db
--- /dev/null
+++ b/doc/tuning/comment_5_0143b798e75ad25c5917794a49a879fb._comment
@@ -0,0 +1,13 @@
+[[!comment format=mdwn
+ username="imlew"
+ avatar="http://cdn.libravatar.org/avatar/23858c3eed3c3ea9e21522f4c999f1ed"
+ subject="Still experimental?"
+ date="2024-03-06T12:26:56Z"
+ content="""
+`annex.tune.objecthash1=true` and `annex.tune.branchhash1=true` seem like they could be helpful in reducing git annex's inode usage, but the disclaimer about this feature being experimental is a little worrying.
+
+Since this it is over 10 years old though, is it still considered experimental or has it graduated to being a stable feature? I.e. will using this meaningfully increase the chance of losing data?
+
+
+Also, what is the (potential) benefit of using lowercase for the hashes?
+"""]]

add reregisterurl command
What this can currently be used for is only to change an url from being
used by a special remote to being used by the web remote.
This could have been a --move-from option to registerurl. But, that would
have complicated its option and --batch processing, and also would have
complicated unregisterurl, which is implemented on top of
Command.Registerurl. So, a separate command was actually less complicated
to implement.
The generic description of the command is because I want to make this
command a catch-all for other url updating kind of things, if there are
ever any more. Also because it was hard to come up with a good name for the
specific action. I considered `git-annex moveurl`, but that seems to
indicate data is perhaps actually being moved, and seems to sit at the same
level as addurl and rmurl, and this command is at the plumbing
level of registerurl and unregisterurl.
Sponsored-by: Dartmouth College's DANDI project
diff --git a/CHANGELOG b/CHANGELOG
index 2bc030c19d..e7a608e51c 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -5,6 +5,8 @@ git-annex (10.20240228) UNRELEASED; urgency=medium
     annexed files be verified with a checksum that is calculated
     on a later download from the web. This will become the default later.
   * Added dependency on unbounded-delays.
+  * reregisterurl: New command that can change an url from being
+    used by a special remote to being used by the web remote.
 
  -- Joey Hess <id@joeyh.name>  Tue, 27 Feb 2024 13:07:10 -0400
 
diff --git a/CmdLine/GitAnnex.hs b/CmdLine/GitAnnex.hs
index 47a37a3de7..debf30fffd 100644
--- a/CmdLine/GitAnnex.hs
+++ b/CmdLine/GitAnnex.hs
@@ -34,6 +34,7 @@ import qualified Command.MatchExpression
 import qualified Command.FromKey
 import qualified Command.RegisterUrl
 import qualified Command.UnregisterUrl
+import qualified Command.ReregisterUrl
 import qualified Command.SetKey
 import qualified Command.DropKey
 import qualified Command.Transferrer
@@ -196,6 +197,7 @@ cmds testoptparser testrunner mkbenchmarkgenerator = map addGitAnnexCommonOption
 	, Command.FromKey.cmd
 	, Command.RegisterUrl.cmd
 	, Command.UnregisterUrl.cmd
+	, Command.ReregisterUrl.cmd
 	, Command.SetKey.cmd
 	, Command.DropKey.cmd
 	, Command.Transferrer.cmd
diff --git a/Command/ReregisterUrl.hs b/Command/ReregisterUrl.hs
new file mode 100644
index 0000000000..5d95f0da43
--- /dev/null
+++ b/Command/ReregisterUrl.hs
@@ -0,0 +1,79 @@
+{- git-annex command
+ -
+ - Copyright 2015-2024 Joey Hess <id@joeyh.name>
+ -
+ - Licensed under the GNU AGPL version 3 or higher.
+ -}
+
+module Command.ReregisterUrl where
+
+import Command
+import Logs.Web
+import Command.FromKey (keyOpt, keyOpt')
+import qualified Remote
+import Git.Types
+
+cmd :: Command
+cmd = withAnnexOptions [jsonOptions] $ command "reregisterurl"
+	SectionPlumbing "updates url registration information"
+	(paramKey)
+	(seek <$$> optParser)
+
+data ReregisterUrlOptions = ReregisterUrlOptions
+	{ keyOpts :: CmdParams
+	, batchOption :: BatchMode
+	, moveFromOption :: Maybe (DeferredParse Remote)
+	}
+
+optParser :: CmdParamsDesc -> Parser ReregisterUrlOptions
+optParser desc = ReregisterUrlOptions
+	<$> cmdParams desc
+	<*> parseBatchOption False
+	<*> optional (mkParseRemoteOption <$> parseMoveFromOption)
+
+parseMoveFromOption :: Parser RemoteName
+parseMoveFromOption = strOption
+	( long "move-from" <> metavar paramRemote
+	<> completeRemotes
+	)
+
+seek :: ReregisterUrlOptions -> CommandSeek
+seek o = case (batchOption o, keyOpts o) of
+	(Batch fmt, _) -> seekBatch o fmt
+	(NoBatch, ps) -> commandAction (start o ps)
+
+seekBatch :: ReregisterUrlOptions -> BatchFormat -> CommandSeek
+seekBatch o fmt = batchOnly Nothing (keyOpts o) $
+	batchInput fmt (pure . parsebatch) $
+		batchCommandAction . start' o
+  where
+	parsebatch l = case keyOpt' l of
+		Left e -> Left e
+		Right k -> Right k
+
+start :: ReregisterUrlOptions -> [String] -> CommandStart
+start o (keyname:[]) = start' o (si, keyOpt keyname)
+  where
+	si = SeekInput [keyname]
+start _ _ = giveup "specify a key"
+
+start' :: ReregisterUrlOptions -> (SeekInput, Key) -> CommandStart
+start' o (si, key) =
+	starting "reregisterurl" ai si $
+		perform o key
+  where
+	ai = ActionItemKey key
+
+perform :: ReregisterUrlOptions -> Key -> CommandPerform
+perform o key = maybe (pure Nothing) (Just <$$> getParsed) (moveFromOption o) >>= \case
+	Nothing -> next $ return True
+	Just r -> do
+		us <- map fst
+			. filter (\(_, d) -> d == OtherDownloader)
+			. map getDownloader
+			<$> getUrls key
+		us' <- filterM (\u -> (== r) <$> Remote.claimingUrl u) us
+		forM_ us' $ \u -> do
+			setUrlMissing key (setDownloader u OtherDownloader)
+			setUrlPresent key u
+		next $ return True
diff --git a/doc/forum/how_to___34__move__34___URL_between_remotes__63__/comment_6_260c238435f8f5e53511de7e574dd53f._comment b/doc/forum/how_to___34__move__34___URL_between_remotes__63__/comment_6_260c238435f8f5e53511de7e574dd53f._comment
new file mode 100644
index 0000000000..f1114fc3ba
--- /dev/null
+++ b/doc/forum/how_to___34__move__34___URL_between_remotes__63__/comment_6_260c238435f8f5e53511de7e574dd53f._comment
@@ -0,0 +1,29 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 6"""
+ date="2024-03-05T18:11:32Z"
+ content="""
+Went far enough down implementing `registerurl --move-from` to be sure that
+it would complicate the code far more than just adding a new `moveurl`
+command. So despite it being a fairly unusual situation, a new command
+is better than that option.
+
+And implemented it:
+
+	joey@darkstar:~/tmp/x>git-annex registerurl WORM--bar http://example.com/bar.fooregisterurl http://example.com/bar.foo ok
+	joey@darkstar:~/tmp/x> git-annex whereis --key WORM--bar
+	whereis WORM--bar (1 copy)
+	  	dca0b5f9-659a-4928-84db-ff9fd74d8fc8 -- [foo]
+	
+	  foo: http://example.com/bar.foo
+	ok
+	joey@darkstar:~/tmp/x>git-annex reregisterurl WORM--bar --move-from=foo
+	reregisterurl WORM--bar ok
+	joey@darkstar:~/tmp/x>git-annex whereis --key WORM--bar
+	whereis WORM--bar (2 copies)
+	  	00000000-0000-0000-0000-000000000001 -- web
+	   	dca0b5f9-659a-4928-84db-ff9fd74d8fc8 -- [foo]
+	
+	  web: http://example.com/bar.foo
+	ok
+"""]]
diff --git a/doc/git-annex-registerurl.mdwn b/doc/git-annex-registerurl.mdwn
index 753d06b704..bf5133b8db 100644
--- a/doc/git-annex-registerurl.mdwn
+++ b/doc/git-annex-registerurl.mdwn
@@ -68,6 +68,8 @@ special remote that claims it. (Usually the web special remote.)
 
 [[git-annex-unregisterurl]](1)
 
+[[git-annex-reregisterurl]](1)
+
 # AUTHOR
 
 Joey Hess <id@joeyh.name>
diff --git a/doc/git-annex-reregisterurl.mdwn b/doc/git-annex-reregisterurl.mdwn
new file mode 100644
index 0000000000..c22a0c6e98
--- /dev/null
+++ b/doc/git-annex-reregisterurl.mdwn
@@ -0,0 +1,64 @@
+# NAME
+
+git-annex reregisterurl - updates url registration information
+
+# SYNOPSIS
+
+git annex reregisterurl `[key]`
+
+# DESCRIPTION
+
+This plumbing-level command updates information about the urls that are
+registered for a key.
+
+# OPTIONS
+
+* `--move-from=name|uuid`
+
+  For each key, update any urls that are currently claimed by the
+  specified remote to be instead used by the web special remote.
+
+  This could be used eg, when a special remote was needed to provide
+  authorization to get an url, but the url has now become publically
+  available and so the web special remote can be used.
+
+  Note that, like `git-annex unregisterurl`, using this option unregisters
+  an url from a special remote, but it does not mark the content as not
+  present in that special remote. However, like `git-annex registerurl`,
+  this option does mark content as being present in the web special remote.

(Diff truncated)
small problem
diff --git a/doc/todo/migration_to_VURL_by_default.mdwn b/doc/todo/migration_to_VURL_by_default.mdwn
index ce78eb857d..7d1c4b4385 100644
--- a/doc/todo/migration_to_VURL_by_default.mdwn
+++ b/doc/todo/migration_to_VURL_by_default.mdwn
@@ -21,3 +21,9 @@ WORM keys if they really want to.
 However, I don't think there's enough reason to want to use URL keys to add
 configuration of which kind of keys addurl uses, once VURL is the default.
 --[[Joey]]
+
+> One way this might cause trouble is that current `git-annex registerurl`
+> and `unregisterurl` (and `fromkey`)  when passed an url rather than a key,
+> generates an URL key. If that is changed to generate a VURL key, then
+> it might break some workflow, particularly one where an url was
+> registered as an URL key and is now being unregistered.