import Utility.Url
import Utility.ResourcePool
import Utility.HumanTime
+import Git.Credential (CredentialCache(..))
import "mtl" Control.Monad.Reader
import Control.Concurrent
, forcebackend :: Maybe String
, useragent :: Maybe String
, desktopnotify :: DesktopNotify
+ , gitcredentialcache :: TMVar CredentialCache
}
newAnnexRead :: GitConfig -> IO AnnexRead
si <- newTVarIO M.empty
tp <- newTransferrerPool
cm <- newTMVarIO M.empty
+ cc <- newTMVarIO (CredentialCache M.empty)
return $ AnnexRead
{ activekeys = emptyactivekeys
, activeremotes = emptyactiveremotes
, forcemincopies = Nothing
, useragent = Nothing
, desktopnotify = mempty
+ , gitcredentialcache = cc
}
-- Values that can change while running an Annex action.
{- git credential interface
-
- - Copyright 2019-2020 Joey Hess <id@joeyh.name>
+ - Copyright 2019-2022 Joey Hess <id@joeyh.name>
-
- Licensed under the GNU AGPL version 3 or higher.
-}
+{-# LANGUAGE OverloadedStrings #-}
+
module Git.Credential where
import Common
import Git
+import Git.Types
import Git.Command
+import qualified Git.Config as Config
import Utility.Url
import qualified Data.Map as M
+import Network.URI
+import Control.Concurrent.STM
data Credential = Credential { fromCredential :: M.Map String String }
<$> credentialUsername cred
<*> credentialPassword cred
-getBasicAuthFromCredential :: Repo -> GetBasicAuth
-getBasicAuthFromCredential r u = do
- c <- getUrlCredential u r
- case credentialBasicAuth c of
- Just ba -> return $ Just (ba, signalsuccess c)
- Nothing -> do
- signalsuccess c False
- return Nothing
+getBasicAuthFromCredential :: Repo -> TMVar CredentialCache -> GetBasicAuth
+getBasicAuthFromCredential r ccv u = do
+ (CredentialCache cc) <- atomically $ readTMVar ccv
+ case mkCredentialBaseURL r u of
+ Just bu -> case M.lookup bu cc of
+ Just c -> go (const noop) c
+ Nothing -> do
+ let storeincache = \c -> atomically $ do
+ (CredentialCache cc') <- takeTMVar ccv
+ putTMVar ccv (CredentialCache (M.insert bu c cc'))
+ go storeincache =<< getUrlCredential u r
+ Nothing -> go (const noop) =<< getUrlCredential u r
where
- signalsuccess c True = approveUrlCredential c r
- signalsuccess c False = rejectUrlCredential c r
+ go storeincache c =
+ case credentialBasicAuth c of
+ Just ba -> return $ Just (ba, signalsuccess)
+ Nothing -> do
+ signalsuccess False
+ return Nothing
+ where
+ signalsuccess True = do
+ () <- storeincache c
+ approveUrlCredential c r
+ signalsuccess False = rejectUrlCredential c r
--- | This may prompt the user for login information, or get cached login
--- information.
+-- | This may prompt the user for the credential, or get a cached
+-- credential from git.
getUrlCredential :: URLString -> Repo -> IO Credential
getUrlCredential = runCredential "fill" . urlCredential
go l = case break (== '=') l of
(k, _:v) -> (k, v)
(k, []) -> (k, "")
+
+-- This is not the cache used by git, but is an in-process cache,
+-- allowing a process to avoid prompting repeatedly when accessing related
+-- urls even when git is not configured to cache credentials.
+data CredentialCache = CredentialCache (M.Map CredentialBaseURL Credential)
+
+-- An url with the uriPath empty when credential.useHttpPath is false.
+--
+-- When credential.useHttpPath is true, no caching is done, since each
+-- distinct url would need a different credential to be cached, which
+-- could cause the CredentialCache to use a lot of memory. Presumably,
+-- when credential.useHttpPath is false, one Credential is cached
+-- for each git repo accessed, and there are a reasonably small number of
+-- those, so the cache will not grow too large.
+data CredentialBaseURL = CredentialBaseURL URI
+ deriving (Show, Eq, Ord)
+
+mkCredentialBaseURL :: Repo -> URLString -> Maybe CredentialBaseURL
+mkCredentialBaseURL r s = do
+ u <- parseURI s
+ let usehttppath = fromMaybe False $ Config.isTrueFalse' $
+ Config.get (ConfigKey "credential.useHttpPath") (ConfigValue "") r
+ if usehttppath
+ then Nothing
+ else Just $ CredentialBaseURL $ u { uriPath = "" }
--- /dev/null
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 3"""
+ date="2022-09-09T18:11:28Z"
+ content="""
+I've implemented this, and a get of multiple files will prompt once.
+
+However, there is one case where the password is prompted twice.
+In a freshly cloned repo, where you have not run `git-annex init` manually,
+`git-annex get foo` will prompt twice.
+
+That is because autoinit causes `git-annex init --autoenable` to be run,
+and that infortunately probes for the UUID of the http remote,
+which needs the password. Since the cache is necessarily only for a single
+process, that subprocess adds an additional prompt.
+
+There might also be other cases where git-annex starts
+subprocesses, that legitimately each need to prompt once for the password.
+I expect that, when `git-annex transferrer` is used
+(due to annex.stalldetection being configured), and -J is used,
+each transferrer process will end up prompting once for the password.
+
+I don't think it makes sense to convert this from a simple in-process cache
+to a cache that is shared amoung all subprocesses. That would reimplement
+what `git-credential-cache` already does. And if you need that,
+you can just enable it.
+
+But I would like to fix the autoinit case to not prompt twice, and am
+leaving this open for now to do that.
+"""]]