remove dead nodes when loading the cluster log

This is to avoid inserting a cluster uuid into the location log when
only dead nodes in the cluster contain the content of a key.

One reason why this is necessary is Remote.keyLocations, which excludes
dead repositories from the list. But there are probably many more.

Implementing this was challenging, because Logs.Location importing
Logs.Cluster which imports Logs.Trust which imports Remote.List resulted
in an import cycle through several other modules.

Resorted to making Logs.Location not import Logs.Cluster, and instead
it assumes that Annex.clusters gets populated when necessary before it's
called.

That's done in Annex.Startup, which is run by the git-annex command
(but not other commands) at early startup in initialized repos. Or,
is run after initialization.

Note that is Remote.Git, it is unable to import Annex.Startup, because
Remote.Git importing Logs.Cluster leads the the same import cycle.
So ensureInitialized is not passed annexStartup in there.

Other commands, like git-annex-shell currently don't run annexStartup
either.

So there are cases where Logs.Location will not see clusters. So it won't add
any cluster UUIDs when loading the log. That's ok, the only reason to do
that is to make display of where objects are located include clusters,
and to make commands like git-annex get --from treat keys as being located
in a cluster. git-annex-shell certainly does not do anything like that,
and I'm pretty sure Remote.Git (and callers to Remote.Git.onLocalRepo)
don't either.
This commit is contained in:
Joey Hess 2024-06-16 14:35:07 -04:00
parent 36c6d8da69
commit 780367200b
No known key found for this signature in database
GPG key ID: DB12DB0FF05F8F38
18 changed files with 137 additions and 67 deletions

View file

@ -5,12 +5,9 @@
- Licensed under the GNU AGPL version 3 or higher. - Licensed under the GNU AGPL version 3 or higher.
-} -}
{-# LANGUAGE CPP #-}
module Annex.Action ( module Annex.Action (
action, action,
verifiedAction, verifiedAction,
startup,
quiesce, quiesce,
stopCoProcesses, stopCoProcesses,
) where ) where
@ -27,11 +24,6 @@ import Annex.CheckIgnore
import Annex.TransferrerPool import Annex.TransferrerPool
import qualified Database.Keys import qualified Database.Keys
#ifndef mingw32_HOST_OS
import Control.Concurrent.STM
import System.Posix.Signals
#endif
{- Runs an action that may throw exceptions, catching and displaying them. -} {- Runs an action that may throw exceptions, catching and displaying them. -}
action :: Annex () -> Annex Bool action :: Annex () -> Annex Bool
action a = tryNonAsync a >>= \case action a = tryNonAsync a >>= \case
@ -47,34 +39,6 @@ verifiedAction a = tryNonAsync a >>= \case
warning (UnquotedString (show e)) warning (UnquotedString (show e))
return (False, UnVerified) return (False, UnVerified)
{- Actions to perform each time ran. -}
startup :: Annex ()
startup = do
#ifndef mingw32_HOST_OS
av <- Annex.getRead Annex.signalactions
let propagate sig = liftIO $ installhandleronce sig av
propagate sigINT
propagate sigQUIT
propagate sigTERM
propagate sigTSTP
propagate sigCONT
propagate sigHUP
-- sigWINCH is not propagated; it should not be needed,
-- and the concurrent-output library installs its own signal
-- handler for it.
-- sigSTOP and sigKILL cannot be caught, so will not be propagated.
where
installhandleronce sig av = void $
installHandler sig (CatchOnce (gotsignal sig av)) Nothing
gotsignal sig av = do
mapM_ (\a -> a (fromIntegral sig)) =<< atomically (readTVar av)
raiseSignal sig
installhandleronce sig av
#else
return ()
#endif
{- Rn all cleanup actions, save all state, stop all long-running child {- Rn all cleanup actions, save all state, stop all long-running child
- processes. - processes.
- -

View file

@ -103,8 +103,8 @@ genDescription Nothing = do
Right username -> [username, at, hostname, ":", reldir] Right username -> [username, at, hostname, ":", reldir]
Left _ -> [hostname, ":", reldir] Left _ -> [hostname, ":", reldir]
initialize :: Maybe String -> Maybe RepoVersion -> Annex () initialize :: Annex () -> Maybe String -> Maybe RepoVersion -> Annex ()
initialize mdescription mversion = checkInitializeAllowed $ \initallowed -> do initialize startupannex mdescription mversion = checkInitializeAllowed $ \initallowed -> do
{- Has to come before any commits are made as the shared {- Has to come before any commits are made as the shared
- clone heuristic expects no local objects. -} - clone heuristic expects no local objects. -}
sharedclone <- checkSharedClone sharedclone <- checkSharedClone
@ -114,7 +114,7 @@ initialize mdescription mversion = checkInitializeAllowed $ \initallowed -> do
ensureCommit $ Annex.Branch.create ensureCommit $ Annex.Branch.create
prepUUID prepUUID
initialize' mversion initallowed initialize' startupannex mversion initallowed
initSharedClone sharedclone initSharedClone sharedclone
@ -129,8 +129,8 @@ initialize mdescription mversion = checkInitializeAllowed $ \initallowed -> do
-- Everything except for uuid setup, shared clone setup, and initial -- Everything except for uuid setup, shared clone setup, and initial
-- description. -- description.
initialize' :: Maybe RepoVersion -> InitializeAllowed -> Annex () initialize' :: Annex () -> Maybe RepoVersion -> InitializeAllowed -> Annex ()
initialize' mversion _initallowed = do initialize' startupannex mversion _initallowed = do
checkLockSupport checkLockSupport
checkFifoSupport checkFifoSupport
checkCrippledFileSystem checkCrippledFileSystem
@ -162,6 +162,10 @@ initialize' mversion _initallowed = do
createInodeSentinalFile False createInodeSentinalFile False
fixupUnusualReposAfterInit fixupUnusualReposAfterInit
-- This is usually run at Annex startup, but when git-annex was
-- not already initialized, it will not yet have run.
startupannex
uninitialize :: Annex () uninitialize :: Annex ()
uninitialize = do uninitialize = do
-- Remove hooks that are written when initializing. -- Remove hooks that are written when initializing.
@ -203,12 +207,12 @@ getInitializedVersion = do
- -
- Checks repository version and handles upgrades too. - Checks repository version and handles upgrades too.
-} -}
ensureInitialized :: Annex [Remote] -> Annex () ensureInitialized :: Annex () -> Annex [Remote] -> Annex ()
ensureInitialized remotelist = getInitializedVersion >>= maybe needsinit checkUpgrade ensureInitialized startupannex remotelist = getInitializedVersion >>= maybe needsinit checkUpgrade
where where
needsinit = ifM autoInitializeAllowed needsinit = ifM autoInitializeAllowed
( do ( do
tryNonAsync (initialize Nothing Nothing) >>= \case tryNonAsync (initialize startupannex Nothing Nothing) >>= \case
Right () -> noop Right () -> noop
Left e -> giveup $ show e ++ "\n" ++ Left e -> giveup $ show e ++ "\n" ++
"git-annex: automatic initialization failed due to above problems" "git-annex: automatic initialization failed due to above problems"
@ -256,15 +260,16 @@ guardSafeToUseRepo a = ifM (inRepo Git.Config.checkRepoConfigInaccessible)
- -
- Checks repository version and handles upgrades too. - Checks repository version and handles upgrades too.
-} -}
autoInitialize :: Annex [Remote] -> Annex () autoInitialize :: Annex () -> Annex [Remote] -> Annex ()
autoInitialize = autoInitialize' autoInitializeAllowed autoInitialize = autoInitialize' autoInitializeAllowed
autoInitialize' :: Annex Bool -> Annex [Remote] -> Annex () autoInitialize' :: Annex Bool -> Annex () -> Annex [Remote] -> Annex ()
autoInitialize' check remotelist = getInitializedVersion >>= maybe needsinit checkUpgrade autoInitialize' check startupannex remotelist =
getInitializedVersion >>= maybe needsinit checkUpgrade
where where
needsinit = needsinit =
whenM (initializeAllowed <&&> check) $ do whenM (initializeAllowed <&&> check) $ do
initialize Nothing Nothing initialize startupannex Nothing Nothing
autoEnableSpecialRemotes remotelist autoEnableSpecialRemotes remotelist
{- Checks if a repository is initialized. Does not check version for upgrade. -} {- Checks if a repository is initialized. Does not check version for upgrade. -}

67
Annex/Startup.hs Normal file
View file

@ -0,0 +1,67 @@
{- git-annex startup
-
- Copyright 2010-2024 Joey Hess <id@joeyh.name>
-
- Licensed under the GNU AGPL version 3 or higher.
-}
{-# LANGUAGE CPP #-}
module Annex.Startup where
import Annex.Common
import qualified Annex
import Logs.Cluster
#ifndef mingw32_HOST_OS
import Control.Concurrent.STM
import System.Posix.Signals
#endif
{- Run when starting up the main git-annex program. -}
startup :: Annex ()
startup = do
startupSignals
gc <- Annex.getGitConfig
when (isinitialized gc)
startupAnnex
where
isinitialized gc = annexUUID gc /= NoUUID
&& isJust (annexVersion gc)
{- Run when starting up the main git-annex program when
- git-annex has already been initialized.
- Alternatively, run after initialization.
-}
startupAnnex :: Annex ()
startupAnnex =
-- Logs.Location needs clusters to be loaded before it is used,
-- in order for a cluster to be treated as the location of keys
-- that are located in any of its nodes.
void loadClusters
startupSignals :: Annex ()
startupSignals = do
#ifndef mingw32_HOST_OS
av <- Annex.getRead Annex.signalactions
let propagate sig = liftIO $ installhandleronce sig av
propagate sigINT
propagate sigQUIT
propagate sigTERM
propagate sigTSTP
propagate sigCONT
propagate sigHUP
-- sigWINCH is not propagated; it should not be needed,
-- and the concurrent-output library installs its own signal
-- handler for it.
-- sigSTOP and sigKILL cannot be caught, so will not be propagated.
where
installhandleronce sig av = void $
installHandler sig (CatchOnce (gotsignal sig av)) Nothing
gotsignal sig av = do
mapM_ (\a -> a (fromIntegral sig)) =<< atomically (readTVar av)
raiseSignal sig
installhandleronce sig av
#else
return ()
#endif

View file

@ -19,6 +19,7 @@ import qualified Annex
import Annex.UUID import Annex.UUID
import Annex.AdjustedBranch import Annex.AdjustedBranch
import Annex.Action import Annex.Action
import Annex.Startup
import Types.StandardGroups import Types.StandardGroups
import Logs.PreferredContent import Logs.PreferredContent
import qualified Annex.Branch import qualified Annex.Branch
@ -85,7 +86,7 @@ initRepo False _ dir desc mgroup = inDir dir $ do
initRepo' :: Maybe String -> Maybe StandardGroup -> Annex () initRepo' :: Maybe String -> Maybe StandardGroup -> Annex ()
initRepo' desc mgroup = unlessM isInitialized $ do initRepo' desc mgroup = unlessM isInitialized $ do
initialize desc Nothing initialize startupAnnex desc Nothing
u <- getUUID u <- getUUID
maybe noop (defaultStandardGroup u) mgroup maybe noop (defaultStandardGroup u) mgroup
{- Ensure branch gets committed right away so it is {- Ensure branch gets committed right away so it is

View file

@ -23,6 +23,7 @@ import qualified Annex
import qualified Git import qualified Git
import qualified Git.AutoCorrect import qualified Git.AutoCorrect
import qualified Git.Config import qualified Git.Config
import Annex.Startup
import Annex.Action import Annex.Action
import Annex.Environment import Annex.Environment
import Command import Command

View file

@ -30,6 +30,7 @@ import qualified Logs.Remote
import qualified Remote.External import qualified Remote.External
import Remote.Helper.Encryptable (parseEncryptionMethod) import Remote.Helper.Encryptable (parseEncryptionMethod)
import Annex.Transfer import Annex.Transfer
import Annex.Startup
import Backend.GitRemoteAnnex import Backend.GitRemoteAnnex
import Config import Config
import Types.Key import Types.Key
@ -1173,7 +1174,7 @@ cleanupInitialization sab alternatejournaldir = void $ tryNonAsync $ do
inRepo $ Git.Branch.delete Annex.Branch.fullname inRepo $ Git.Branch.delete Annex.Branch.fullname
ifM (Annex.Branch.hasSibling <&&> nonbuggygitversion) ifM (Annex.Branch.hasSibling <&&> nonbuggygitversion)
( do ( do
autoInitialize' (pure True) remoteList autoInitialize' (pure True) startupAnnex remoteList
differences <- allDifferences <$> recordedDifferences differences <- allDifferences <$> recordedDifferences
when (differences /= mempty) $ when (differences /= mempty) $
deletebundleobjects deletebundleobjects

View file

@ -23,6 +23,7 @@ import CmdLine.Batch as ReExported
import Options.Applicative as ReExported hiding (command) import Options.Applicative as ReExported hiding (command)
import qualified Git import qualified Git
import Annex.Init import Annex.Init
import Annex.Startup
import Utility.Daemon import Utility.Daemon
import Types.Transfer import Types.Transfer
import Types.ActionItem as ReExported import Types.ActionItem as ReExported
@ -125,7 +126,7 @@ commonChecks :: [CommandCheck]
commonChecks = [repoExists] commonChecks = [repoExists]
repoExists :: CommandCheck repoExists :: CommandCheck
repoExists = CommandCheck RepoExists (ensureInitialized remoteList) repoExists = CommandCheck RepoExists (ensureInitialized startupAnnex remoteList)
notBareRepo :: Command -> Command notBareRepo :: Command -> Command
notBareRepo = addCheck CheckNotBareRepo checkNotBareRepo notBareRepo = addCheck CheckNotBareRepo checkNotBareRepo

View file

@ -17,6 +17,7 @@ import qualified BuildInfo
import Utility.HumanTime import Utility.HumanTime
import Assistant.Install import Assistant.Install
import Remote.List import Remote.List
import Annex.Startup
import Control.Concurrent.Async import Control.Concurrent.Async
@ -63,7 +64,7 @@ start o
stop stop
| otherwise = do | otherwise = do
liftIO ensureInstalled liftIO ensureInstalled
ensureInitialized remoteList ensureInitialized startupAnnex remoteList
Command.Watch.start True (daemonOptions o) (startDelayOption o) Command.Watch.start True (daemonOptions o) (startDelayOption o)
startNoRepo :: AssistantOptions -> IO () startNoRepo :: AssistantOptions -> IO ()

View file

@ -16,6 +16,7 @@ import Git.Types
import Remote.GCrypt (coreGCryptId) import Remote.GCrypt (coreGCryptId)
import qualified CmdLine.GitAnnexShell.Fields as Fields import qualified CmdLine.GitAnnexShell.Fields as Fields
import CmdLine.GitAnnexShell.Checks import CmdLine.GitAnnexShell.Checks
import Annex.Startup
cmd :: Command cmd :: Command
cmd = noCommit $ dontCheck repoExists $ cmd = noCommit $ dontCheck repoExists $
@ -47,7 +48,7 @@ findOrGenUUID = do
else ifM (Annex.Branch.hasSibling <||> (isJust <$> Fields.getField Fields.autoInit)) else ifM (Annex.Branch.hasSibling <||> (isJust <$> Fields.getField Fields.autoInit))
( do ( do
liftIO checkNotReadOnly liftIO checkNotReadOnly
initialize Nothing Nothing initialize startupAnnex Nothing Nothing
getUUID getUUID
, return NoUUID , return NoUUID
) )

View file

@ -12,6 +12,7 @@ module Command.Init where
import Command import Command
import Annex.Init import Annex.Init
import Annex.Version import Annex.Version
import Annex.Startup
import Types.RepoVersion import Types.RepoVersion
import qualified Annex.SpecialRemote import qualified Annex.SpecialRemote
@ -77,7 +78,7 @@ perform os = do
Just v | v /= wantversion -> Just v | v /= wantversion ->
giveup $ "This repository is already a initialized with version " ++ show (fromRepoVersion v) ++ ", not changing to requested version." giveup $ "This repository is already a initialized with version " ++ show (fromRepoVersion v) ++ ", not changing to requested version."
_ -> noop _ -> noop
initialize initialize startupAnnex
(if null (initDesc os) then Nothing else Just (initDesc os)) (if null (initDesc os) then Nothing else Just (initDesc os))
(initVersion os) (initVersion os)
unless (noAutoEnable os) unless (noAutoEnable os)

View file

@ -10,6 +10,7 @@ module Command.Reinit where
import Command import Command
import Annex.Init import Annex.Init
import Annex.UUID import Annex.UUID
import Annex.Startup
import qualified Remote import qualified Remote
import qualified Annex.SpecialRemote import qualified Annex.SpecialRemote
@ -36,6 +37,6 @@ perform s = do
then return $ toUUID s then return $ toUUID s
else Remote.nameToUUID s else Remote.nameToUUID s
storeUUID u storeUUID u
checkInitializeAllowed $ initialize' Nothing checkInitializeAllowed $ initialize' startupAnnex Nothing
Annex.SpecialRemote.autoEnable Annex.SpecialRemote.autoEnable
next $ return True next $ return True

View file

@ -11,6 +11,7 @@ import Command
import Upgrade import Upgrade
import Annex.Version import Annex.Version
import Annex.Init import Annex.Init
import Annex.Startup
cmd :: Command cmd :: Command
cmd = dontCheck cmd = dontCheck
@ -46,6 +47,6 @@ start (UpgradeOptions { autoOnly = True }) =
start _ = start _ =
starting "upgrade" (ActionItemOther Nothing) (SeekInput []) $ do starting "upgrade" (ActionItemOther Nothing) (SeekInput []) $ do
whenM (isNothing <$> getVersion) $ do whenM (isNothing <$> getVersion) $ do
initialize Nothing Nothing initialize startupAnnex Nothing Nothing
r <- upgrade False latestVersion r <- upgrade False latestVersion
next $ return r next $ return r

View file

@ -13,6 +13,7 @@ module Logs.Cluster (
fromClusterUUID, fromClusterUUID,
ClusterNodeUUID(..), ClusterNodeUUID(..),
getClusters, getClusters,
loadClusters,
recordCluster, recordCluster,
Clusters(..) Clusters(..)
) where ) where
@ -24,6 +25,7 @@ import Types.Cluster
import Logs import Logs
import Logs.UUIDBased import Logs.UUIDBased
import Logs.MapLog import Logs.MapLog
import Logs.Trust
import qualified Data.Set as S import qualified Data.Set as S
import qualified Data.Map as M import qualified Data.Map as M
@ -35,13 +37,15 @@ import qualified Data.ByteString.Lazy as L
getClusters :: Annex Clusters getClusters :: Annex Clusters
getClusters = maybe loadClusters return =<< Annex.getState Annex.clusters getClusters = maybe loadClusters return =<< Annex.getState Annex.clusters
{- Loads the clusters and caches it for later. -}
loadClusters :: Annex Clusters loadClusters :: Annex Clusters
loadClusters = do loadClusters = do
m <- convclusteruuids . M.map value . fromMapLog . parseClusterLog m <- convclusteruuids . M.map value . fromMapLog . parseClusterLog
<$> Annex.Branch.get clusterLog <$> Annex.Branch.get clusterLog
m' <- removedeadnodes m
let clusters = Clusters let clusters = Clusters
{ clusterUUIDs = m { clusterUUIDs = m'
, clusterNodeUUIDs = M.foldlWithKey inverter mempty m , clusterNodeUUIDs = M.foldlWithKey inverter mempty m'
} }
Annex.changeState $ \s -> s { Annex.clusters = Just clusters } Annex.changeState $ \s -> s { Annex.clusters = Just clusters }
return clusters return clusters
@ -53,6 +57,14 @@ loadClusters = do
inverter m k v = M.unionWith (<>) m inverter m k v = M.unionWith (<>) m
(M.fromList (map (, S.singleton k) (S.toList v))) (M.fromList (map (, S.singleton k) (S.toList v)))
-- Dead nodes are removed from clusters to avoid inserting the
-- cluster uuid into the location log when only dead nodes contain
-- the content of a key.
removedeadnodes m = do
dead <- (S.fromList . map ClusterNodeUUID)
<$> trustGet DeadTrusted
return $ M.map (`S.difference` dead) m
recordCluster :: ClusterUUID -> S.Set ClusterNodeUUID -> Annex () recordCluster :: ClusterUUID -> S.Set ClusterNodeUUID -> Annex ()
recordCluster clusteruuid nodeuuids = do recordCluster clusteruuid nodeuuids = do
-- If a private UUID has been configured as a cluster node, -- If a private UUID has been configured as a cluster node,

View file

@ -41,7 +41,7 @@ import Annex.Common
import qualified Annex.Branch import qualified Annex.Branch
import Logs import Logs
import Logs.Presence import Logs.Presence
import Logs.Cluster import Types.Cluster
import Annex.UUID import Annex.UUID
import Annex.CatFile import Annex.CatFile
import Annex.VectorClock import Annex.VectorClock
@ -248,3 +248,8 @@ overLocationLogs' iv discarder keyaction = do
Annex.Branch.overBranchFileContents getk (go iv) >>= \case Annex.Branch.overBranchFileContents getk (go iv) >>= \case
Just r -> return r Just r -> return r
Nothing -> giveup "This repository is read-only, and there are unmerged git-annex branches, which prevents operating on allu keys. (Set annex.merge-annex-branches to false to ignore the unmerged git-annex branches.)" Nothing -> giveup "This repository is read-only, and there are unmerged git-annex branches, which prevents operating on allu keys. (Set annex.merge-annex-branches to false to ignore the unmerged git-annex branches.)"
-- Cannot import Logs.Cluster due to a cycle.
-- Annex.clusters gets populated when starting up git-annex.
getClusters :: Annex Clusters
getClusters = fromMaybe noClusters <$> Annex.getState Annex.clusters

View file

@ -345,7 +345,7 @@ tryGitConfigRead autoinit r hasuuid
readlocalannexconfig = do readlocalannexconfig = do
let check = do let check = do
Annex.BranchState.disableUpdate Annex.BranchState.disableUpdate
catchNonAsync (autoInitialize (pure [])) $ \e -> catchNonAsync (autoInitialize noop (pure [])) $ \e ->
warning $ UnquotedString $ "Remote " ++ Git.repoDescribe r ++ warning $ UnquotedString $ "Remote " ++ Git.repoDescribe r ++
": " ++ show e ": " ++ show e
Annex.getState Annex.repo Annex.getState Annex.repo
@ -601,7 +601,7 @@ repairRemote r a = return $ do
s <- Annex.new r s <- Annex.new r
Annex.eval s $ do Annex.eval s $ do
Annex.BranchState.disableUpdate Annex.BranchState.disableUpdate
ensureInitialized (pure []) ensureInitialized noop (pure [])
a `finally` quiesce True a `finally` quiesce True
data LocalRemoteAnnex = LocalRemoteAnnex Git.Repo (MVar [(Annex.AnnexState, Annex.AnnexRead)]) data LocalRemoteAnnex = LocalRemoteAnnex Git.Repo (MVar [(Annex.AnnexState, Annex.AnnexRead)])
@ -645,7 +645,7 @@ onLocal' (LocalRemoteAnnex repo mv) a = liftIO (takeMVar mv) >>= \case
[] -> do [] -> do
liftIO $ putMVar mv [] liftIO $ putMVar mv []
v <- newLocal repo v <- newLocal repo
go (v, ensureInitialized (pure []) >> a) go (v, ensureInitialized noop (pure []) >> a)
(v:rest) -> do (v:rest) -> do
liftIO $ putMVar mv rest liftIO $ putMVar mv rest
go (v, a) go (v, a)

View file

@ -13,9 +13,9 @@ module Types.Cluster (
genClusterUUID, genClusterUUID,
fromClusterUUID, fromClusterUUID,
isClusterUUID, isClusterUUID,
fromClusterUUID,
ClusterNodeUUID(..), ClusterNodeUUID(..),
Clusters(..), Clusters(..),
noClusters,
) where ) where
import Types.UUID import Types.UUID
@ -79,3 +79,6 @@ data Clusters = Clusters
, clusterNodeUUIDs :: M.Map ClusterNodeUUID (S.Set ClusterUUID) , clusterNodeUUIDs :: M.Map ClusterNodeUUID (S.Set ClusterUUID)
} }
deriving (Show) deriving (Show)
noClusters :: Clusters
noClusters = Clusters mempty mempty

View file

@ -45,10 +45,14 @@ For June's work on [[design/passthrough_proxy]], implementation plan:
* Implement cluster UUID insertation on location log load, and removal * Implement cluster UUID insertation on location log load, and removal
on location log store. (done) on location log store. (done)
* Omit cluster UUIDs when constructing drop proofs, since lockcontent will
always fail on a cluster. (done)
* Don't count cluster UUID as a copy. (Including in `whereis` display.) * Don't count cluster UUID as a copy. (Including in `whereis` display.)
* Omit cluster UUIDs when constructing drop proofs, since lockcontent will Work in progress. fromNumCopies is sometimes used to get a
always fail on a cluster. number that is compared with a list of UUIDs. And limitCopies doesn't
use numcopies machinery
* Basic proxying to special remote support (non-streaming). * Basic proxying to special remote support (non-streaming).

View file

@ -556,6 +556,7 @@ Executable git-annex
Annex.SpecialRemote.Config Annex.SpecialRemote.Config
Annex.Ssh Annex.Ssh
Annex.StallDetection Annex.StallDetection
Annex.Startup
Annex.TaggedPush Annex.TaggedPush
Annex.Tmp Annex.Tmp
Annex.Transfer Annex.Transfer