mirror of https://github.com/voidlizard/hbs2
352 lines
11 KiB
Haskell
352 lines
11 KiB
Haskell
module HBS2.Git.Client.Export (export) where
|
||
|
||
|
||
import HBS2.Git.Client.Prelude hiding (info)
|
||
import HBS2.Git.Client.App.Types
|
||
import HBS2.Git.Client.Manifest
|
||
import HBS2.Git.Client.RefLog
|
||
import HBS2.Git.Client.State
|
||
import HBS2.Git.Client.Progress
|
||
|
||
import HBS2.Git.Data.RefLog
|
||
import HBS2.Git.Data.Tx.Git
|
||
import HBS2.Git.Data.LWWBlock
|
||
import HBS2.Git.Data.GK
|
||
|
||
import HBS2.Git.Local.CLI
|
||
|
||
import HBS2.KeyMan.Keys.Direct
|
||
|
||
import HBS2.OrDie
|
||
import HBS2.Storage.Operations.ByteString
|
||
import HBS2.System.Dir
|
||
|
||
import Text.InterpolatedString.Perl6 (qc)
|
||
import Data.Text qualified as Text
|
||
import Data.ByteString.Lazy.Char8 qualified as LBS8
|
||
import Data.ByteString.Char8 qualified as BS8
|
||
import Data.ByteString.Lazy qualified as LBS
|
||
import Data.ByteString.Builder as B
|
||
import Data.HashSet qualified as HashSet
|
||
import Data.HashMap.Strict qualified as HashMap
|
||
import Data.Maybe
|
||
import Data.List qualified as L
|
||
import Data.Ord (comparing)
|
||
import Data.Either
|
||
|
||
data ExportError = ExportUnsupportedOperation
|
||
| ExportBundleCreateError
|
||
deriving stock (Show,Typeable)
|
||
|
||
instance Exception ExportError
|
||
|
||
instance HasErrorStatus ExportError where
|
||
getStatus = \case
|
||
ExportUnsupportedOperation -> Failed
|
||
ExportBundleCreateError -> Failed
|
||
|
||
instance ToFilePath (GitRef, GitHash) where
|
||
toFilePath (g, r) = show (pretty g)
|
||
|
||
{-# ANN module "HLint: ignore Eta reduce" #-}
|
||
filterPat :: ToFilePath a => [FilePattern] -> [FilePattern] -> [a] -> [a]
|
||
filterPat inc excl refs = filter check refs
|
||
where
|
||
check r = i || not e
|
||
where
|
||
e = not $ L.null $ catMaybes [ match p (toFilePath r) | p <- excl ]
|
||
i = not $ L.null $ catMaybes [ match p (toFilePath r) | p <- inc ]
|
||
|
||
refsForExport :: (MonadReader GitEnv m, MonadIO m) => [(GitRef, Maybe GitHash)] -> m [(GitRef,GitHash)]
|
||
|
||
refsForExport forPushL = do
|
||
{- HLINT ignore "Functor law" -}
|
||
|
||
conf <- asks _config
|
||
path <- asks _gitPath
|
||
|
||
let tags = headDef mempty [ "--tags" :: String | (ListVal [SymbolVal "export", SymbolVal "tags"] ) <- conf]
|
||
|
||
let incl = [ Text.unpack p
|
||
| (ListVal [SymbolVal "export", SymbolVal "include", LitStrVal p]) <- conf
|
||
]
|
||
|
||
let excl = [ Text.unpack p
|
||
| (ListVal [SymbolVal "export", SymbolVal "exclude", LitStrVal p]) <- conf
|
||
]
|
||
|
||
let forPush = [ (k,v) | (k, Just v) <- forPushL ] & HashMap.fromList
|
||
|
||
let deleted = [ k | (k, Nothing) <- forPushL ] & HashSet.fromList
|
||
|
||
debug $ red "CONF" <> pretty path <> line <> indent 2 (vcat (fmap pretty conf))
|
||
|
||
let cmd = [qc|git --git-dir={path} show-ref {tags} --heads --head|]
|
||
|
||
debug $ red "CMD" <+> pretty cmd
|
||
debug $ "FILTERS" <+> pretty (incl, excl)
|
||
debug $ red "DELETED" <+> pretty (HashSet.toList deleted)
|
||
debug $ red "FOR-PUSH" <+> pretty (HashMap.toList forPush)
|
||
|
||
-- мы экспортируем всегда HEAD что бы правильно работал git clone
|
||
-- поэтому мы экспортируем и текущий бранч тоже
|
||
-- даже если он запрещён фильтрами
|
||
|
||
currentBranch <- gitRunCommand [qc|git --git-dir={path} symbolic-ref HEAD|]
|
||
>>= orThrowUser "can't read HEAD 1"
|
||
<&> GitRef . BS8.strip . LBS8.toStrict
|
||
|
||
currentVal <- gitRunCommand [qc|git --git-dir={path} rev-parse {pretty currentBranch}|]
|
||
>>= orThrowUser "can't read HEAD 2"
|
||
<&> (BS8.unpack . BS8.strip . LBS8.toStrict)
|
||
<&> fromStringMay @GitHash
|
||
>>= orThrowUser "invalid git hash for HEAD"
|
||
|
||
gitRunCommand cmd
|
||
>>= orThrowUser ("can't read git repo" <+> pretty path)
|
||
<&> LBS8.lines
|
||
<&> fmap LBS8.words
|
||
<&> mapMaybe \case
|
||
[val,name] -> (GitRef (LBS8.toStrict name),) <$> fromStringMay @GitHash (LBS8.unpack val)
|
||
_ -> Nothing
|
||
<&> HashMap.fromList
|
||
<&> HashMap.mapWithKey (\k v -> if k `HashSet.member` deleted then gitHashTomb else v)
|
||
<&> mappend forPush
|
||
<&> mappend (HashMap.singleton currentBranch currentVal)
|
||
<&> HashMap.toList
|
||
<&> L.sortBy orderRefs
|
||
|
||
where
|
||
orderRefs (GitRef "HEAD", _) _ = LT
|
||
orderRefs _ (GitRef "HEAD", _) = GT
|
||
orderRefs x y = comparing fst x y
|
||
|
||
loadNewGK0 :: (MonadIO m, MonadReader GitEnv m)
|
||
=> RefLogId
|
||
-> Maybe HashRef
|
||
-> m (Maybe (HashRef,Epoch))
|
||
|
||
loadNewGK0 r = \case
|
||
Nothing -> storeNewGK0
|
||
|
||
Just tx0 -> do
|
||
href <- storeNewGK0
|
||
withState do
|
||
for_ href (insertNewGK0 r tx0 . fst)
|
||
commitAll
|
||
|
||
withState $ selectNewGK0 r
|
||
|
||
storeNewGK0 :: (MonadIO m, MonadReader GitEnv m) => m (Maybe (HashRef,Epoch))
|
||
storeNewGK0 = do
|
||
sto <- asks _storage
|
||
enc <- asks _gitExportEnc
|
||
runMaybeT do
|
||
gk <- case enc of
|
||
ExportPrivate f -> loadGK0FromFile f >>= toMPlus
|
||
ExportPrivateGK k -> toMPlus $ Just k
|
||
_ -> toMPlus Nothing
|
||
epoch <- getEpoch
|
||
writeAsMerkle sto (serialise gk) <&> HashRef <&> (,epoch)
|
||
|
||
export :: ( GitPerks m
|
||
, MonadReader GitEnv m
|
||
, GroupKeyOperations m
|
||
, HasAPI PeerAPI UNIX m
|
||
)
|
||
=> LWWRefKey 'HBS2Basic
|
||
-> [(GitRef,Maybe GitHash)]
|
||
-> m ()
|
||
export key refs = do
|
||
|
||
git <- asks _gitPath
|
||
sto <- asks _storage
|
||
new <- asks _gitExportType <&> (== ExportNew)
|
||
manifestUpdateEnv <- asks _gitManifestUpdateEnv
|
||
reflog <- asks _refLogAPI
|
||
ip <- asks _progress
|
||
|
||
subscribeLWWRef key
|
||
|
||
(lww, LWWBlockData{..}) <- waitOrInitLWWRef
|
||
|
||
let puk0 = fromLwwRefKey key
|
||
|
||
debug $ red $ pretty $ AsBase58 lwwRefLogPubKey
|
||
|
||
(sk0,pk0) <- liftIO $ runKeymanClient do
|
||
creds <- loadCredentials puk0
|
||
>>= orThrowUser ("can't load credentials" <+> pretty (AsBase58 puk0))
|
||
pure ( view peerSignSk creds, view peerSignPk creds )
|
||
|
||
(puk,sk) <- derivedKey @'HBS2Basic @'Sign lwwRefSeed sk0
|
||
|
||
subscribeRefLog puk
|
||
|
||
myrefs <- refsForExport refs
|
||
|
||
let myrefsKey = L.sortOn fst myrefs & serialise & hashObject @HbSync & HashRef
|
||
|
||
flip runContT pure do
|
||
callCC \exit -> do
|
||
|
||
|
||
tx0 <- getLastAppliedTx
|
||
|
||
rh <- runMaybeT ( toMPlus tx0 >>= readRepoHeadFromTx sto >>= toMPlus )
|
||
|
||
let rh0 = snd <$> rh
|
||
|
||
(name,brief,mf) <- case manifestUpdateEnv of
|
||
-- TODO: do not update manifest if not needed
|
||
Nothing -> lift $ getLastManifestFromStorage key
|
||
Just (ManifestUpdateEnv manifest) -> pure manifest
|
||
|
||
gk0new0 <- loadNewGK0 puk tx0
|
||
|
||
let gk0old = _repoHeadGK0 =<< rh0
|
||
|
||
mbTxTime0 <- runMaybeT $ toMPlus tx0
|
||
>>= withState .selectTxForRefLog puk
|
||
>>= toMPlus
|
||
|
||
-- смотрим, какое время ключа для данного рефлога, т.к. голова-то
|
||
-- может быть одна, а вот рефлоги -- разные
|
||
-- если мы успели --- то накатываем свой ключ.
|
||
-- если нет -- придется повторить
|
||
let gk0new = if (snd <$> gk0new0) > (snd <$> mbTxTime0) then
|
||
fst <$> gk0new0
|
||
else
|
||
gk0old
|
||
|
||
let gk0 = gk0new <|> gk0old
|
||
|
||
repohead <- makeRepoHeadSimple name brief mf gk0 myrefs
|
||
|
||
let oldRefs = maybe mempty repoHeadRefs' rh0
|
||
|
||
trace $ "TX0" <+> pretty tx0
|
||
|
||
bss <- maybe (pure mempty) txBundles tx0
|
||
|
||
objs <- lift enumAllGitObjects
|
||
>>= withState . filterM (notInTx tx0)
|
||
|
||
let updateManifest = isJust manifestUpdateEnv
|
||
|
||
when (null objs && not new && oldRefs == myrefs && not updateManifest) do
|
||
exit ()
|
||
|
||
debug $ red "REFS-FOR-EXPORT:" <+> pretty myrefs
|
||
|
||
done <- withState (selectBundleByKey puk myrefsKey)
|
||
|
||
out <-
|
||
if isJust done && not new then do
|
||
pure []
|
||
|
||
else do
|
||
|
||
p <- ContT $ withGitPack
|
||
|
||
for_ (zip [1..] objs) $ \(n,o) -> do
|
||
onProgress ip (ExportWriteObject (Progress n Nothing))
|
||
liftIO $ LBS8.hPutStrLn (getStdin p) (LBS8.pack $ show $ pretty o)
|
||
|
||
code <- hFlush (getStdin p) >> hClose (getStdin p) >> getExitCode p
|
||
|
||
let idx = serialise objs
|
||
let size = B.word32BE (fromIntegral $ LBS.length idx)
|
||
let hdr = B.word32BE 1
|
||
pack <- liftIO $ LBS.hGetContents (getStdout p)
|
||
let out = B.toLazyByteString ( size <> hdr <> B.lazyByteString idx <> B.lazyByteString pack )
|
||
pure [out]
|
||
|
||
rank <- getEpoch <&> fromIntegral
|
||
|
||
let rw = gk0new /= gk0old
|
||
|
||
debug $ red "MAKE TX" <+> pretty rw <+> pretty gk0old <+> "->" <+> pretty gk0new
|
||
|
||
tx <- lift $ makeTx sto rw rank puk (const $ pure (Just sk)) repohead bss out
|
||
|
||
r <- lift $ race (pause @'Seconds 1) (callService @RpcRefLogPost reflog tx)
|
||
>>= orThrowUser "hbs2-peer rpc timeout"
|
||
|
||
when (isLeft r) $ exit ()
|
||
|
||
void $ runMaybeT do
|
||
(_,_,bh) <- unpackTx tx
|
||
withState (insertBundleKey puk myrefsKey bh)
|
||
|
||
where
|
||
|
||
findSK pk = liftIO $ runKeymanClient $ runMaybeT do
|
||
creds <- lift (loadCredentials pk) >>= toMPlus
|
||
pure (view peerSignSk creds)
|
||
|
||
waitOrInitLWWRef = do
|
||
sto <- asks _storage
|
||
new <- asks _gitExportType <&> (== ExportNew)
|
||
|
||
flip fix 3 $ \next n -> do
|
||
blk <- readLWWBlock sto key
|
||
|
||
case blk of
|
||
Just x -> pure x
|
||
|
||
Nothing | new && n > 0 -> do
|
||
_ <- runExceptT (initLWWRef sto Nothing findSK key)
|
||
>>= either ( throwIO . userError . show ) pure
|
||
|
||
next (pred n)
|
||
|
||
| otherwise -> do
|
||
-- FIXME: detailed-error-description
|
||
orThrowUser "lwwref not available" Nothing
|
||
|
||
|
||
notInTx Nothing _ = pure True
|
||
notInTx (Just tx0) obj = not <$> isObjectInTx tx0 obj
|
||
|
||
getLastAppliedTx = runMaybeT do
|
||
(tx0,_) <- withState selectMaxAppliedTx
|
||
>>= toMPlus
|
||
pure tx0
|
||
|
||
txBundles tx0 = withDef =<< runMaybeT do
|
||
|
||
new <- asks _gitExportType <&> (== ExportNew)
|
||
sto <- asks _storage
|
||
|
||
txbody <- runExceptT (readTx sto tx0)
|
||
>>= orThrowUser ("missed blocks for tx" <+> pretty tx0)
|
||
|
||
let bref = view _4 txbody
|
||
|
||
readBundleRefs sto bref
|
||
>>= orThrowUser ("missed blocks for tx" <+> pretty tx0)
|
||
|
||
where
|
||
withDef Nothing = pure mempty
|
||
withDef (Just x) = pure x
|
||
|
||
enumAllGitObjects :: (GitPerks m, MonadReader GitEnv m) => m [GitHash]
|
||
enumAllGitObjects = do
|
||
path <- asks _gitPath
|
||
let rcmd = [qc|git --git-dir {path} cat-file --batch-check='%(objectname)' --batch-all-objects|]
|
||
(_, out, _) <- liftIO $ readProcess (shell rcmd)
|
||
pure $ LBS8.lines out & mapMaybe (fromStringMay @GitHash . LBS8.unpack)
|
||
|
||
|
||
withGitPack :: (GitPerks m, MonadReader GitEnv m) => (Process Handle Handle () -> m a) -> m a
|
||
withGitPack action = do
|
||
fp <- asks _gitPath
|
||
let cmd = "git"
|
||
let args = ["--git-dir", fp, "pack-objects", "--stdout", "-q"]
|
||
let config = setStdin createPipe $ setStdout createPipe $ setStderr closed $ proc cmd args
|
||
p <- startProcess config
|
||
action p
|
||
|
||
|