new hbs2-git

This commit is contained in:
Dmitry Zuikov 2024-03-07 16:50:16 +03:00
parent da42a1dc69
commit 29e7a1e2fd
21 changed files with 2699 additions and 0 deletions

View File

@ -15,6 +15,8 @@ BINS := \
hbs2-git-reposync \ hbs2-git-reposync \
git-remote-hbs2 \ git-remote-hbs2 \
git-hbs2 \ git-hbs2 \
git-remote-hbs21 \
git-hbs21 \
ifeq ($(origin .RECIPEPREFIX), undefined) ifeq ($(origin .RECIPEPREFIX), undefined)
$(error This Make does not support .RECIPEPREFIX. Please use GNU Make 4.0 or later) $(error This Make does not support .RECIPEPREFIX. Please use GNU Make 4.0 or later)

View File

@ -37,6 +37,7 @@ outputs = { self, nixpkgs, haskell-flake-utils, ... }@inputs:
"hbs2-qblf" "hbs2-qblf"
"hbs2-keyman" "hbs2-keyman"
"hbs2-share" "hbs2-share"
"hbs21-git"
]; ];
in in
haskell-flake-utils.lib.simpleCabalProject2flake { haskell-flake-utils.lib.simpleCabalProject2flake {
@ -60,6 +61,7 @@ outputs = { self, nixpkgs, haskell-flake-utils, ... }@inputs:
"hbs2-keyman" = "./hbs2-keyman"; "hbs2-keyman" = "./hbs2-keyman";
"hbs2-share" = "./hbs2-share"; "hbs2-share" = "./hbs2-share";
"hbs2-git" = "./hbs2-git"; "hbs2-git" = "./hbs2-git";
"hbs21-git" = "./hbs21-git";
"hbs2-git-reposync" = "./hbs2-git-reposync"; "hbs2-git-reposync" = "./hbs2-git-reposync";
}; };

0
hbs21-git/LICENSE Normal file
View File

209
hbs21-git/git-hbs21/Main.hs Normal file
View File

@ -0,0 +1,209 @@
{-# Language UndecidableInstances #-}
module Main where
import HBS2.Git.Client.Prelude hiding (info)
import HBS2.Git.Client.App
import HBS2.Git.Client.Export
import HBS2.Git.Client.Import
import HBS2.Git.Client.State
import HBS2.Git.Data.RefLog
import HBS2.Git.Local.CLI qualified as Git
import HBS2.Git.Data.Tx qualified as TX
import HBS2.Git.Data.Tx (RepoHead(..))
import HBS2.Git.Data.GK
import HBS2.Storage.Operations.ByteString
import Options.Applicative as O
import Data.ByteString.Lazy qualified as LBS
import System.Exit
globalOptions :: Parser [GitOption]
globalOptions = do
t <- flag [] [GitTrace]
( long "trace" <> short 't' <> help "allow trace"
)
d <- flag [] [GitDebug]
( long "debug" <> short 'd' <> help "allow debug"
)
pure (t <> d)
commands :: GitPerks m => Parser (GitCLI m ())
commands =
hsubparser ( command "export" (info pExport (progDesc "export repo to hbs2-git"))
<> command "import" (info pImport (progDesc "import repo from reflog"))
<> command "key" (info pKey (progDesc "key management"))
<> command "tools" (info pTools (progDesc "misc tools"))
)
pRefLogId :: ReadM RefLogId
pRefLogId = maybeReader (fromStringMay @RefLogId)
pHashRef :: ReadM HashRef
pHashRef = maybeReader (fromStringMay @HashRef)
pInit :: GitPerks m => Parser (GitCLI m ())
pInit = do
pure runDefault
pExport :: GitPerks m => Parser (GitCLI m ())
pExport = do
puk <- argument pRefLogId (metavar "REFLOG-KEY")
et <- flag ExportInc ExportNew
( long "new" <> help "new is usable to export to a new empty reflog"
)
enc <- flag' ExportPublic (long "public" <> help "create unencrypted reflog")
<|>
( ExportPrivate <$>
strOption (long "encrypted" <> help "create encrypted reflog"
<> metavar "GROUP-KEY-FILE")
)
pure do
git <- Git.findGitDir >>= orThrowUser "not a git dir"
notice (green "git dir" <+> pretty git <+> pretty (AsBase58 puk))
env <- ask
withGitEnv ( env & set gitApplyHeads False & set gitExportType et & set gitExportEnc enc) do
notice $ red (viaShow et)
unless (et == ExportNew) do
importRepoWait puk
export puk mempty
pImport :: GitPerks m => Parser (GitCLI m ())
pImport = do
puk <- argument pRefLogId (metavar "REFLOG-KEY")
pure do
git <- Git.findGitDir >>= orThrowUser "not a git dir"
importRepoWait puk
pTools :: GitPerks m => Parser (GitCLI m ())
pTools = hsubparser ( command "dump-pack" (info pDumpPack (progDesc "dump hbs2 git pack"))
<> command "show-ref" (info pShowRef (progDesc "show current references"))
)
data DumpOpt = DumpInfoOnly | DumpObjects | DumpPack
pDumpPack :: GitPerks m => Parser (GitCLI m ())
pDumpPack = do
what <- dumpInfoOnly <|> dumpObjects <|> dumpPack
pure do
co <- liftIO LBS.getContents
(idSize,idVer,sidx,pack) <- TX.unpackPackMay co
& orThrowUser "can't unpack the bundle"
case what of
DumpInfoOnly -> do
liftIO $ print $ pretty "version:" <+> pretty idVer <> line
<> "index size:" <+> pretty idSize <> line
<> "objects:" <+> pretty (length sidx)
DumpObjects -> do
liftIO $ print $ vcat (fmap pretty sidx)
DumpPack -> do
liftIO $ LBS.putStr pack
where
dumpInfoOnly = flag DumpInfoOnly DumpInfoOnly
( long "info-only" )
dumpObjects = flag DumpObjects DumpObjects
( long "objects" )
dumpPack = flag DumpPack DumpPack
( long "pack" )
pShowRef :: GitPerks m => Parser (GitCLI m ())
pShowRef = do
pure do
sto <- asks _storage
void $ runMaybeT do
tx <- withState do
selectMaxAppliedTx >>= lift . toMPlus <&> fst
rh <- TX.readRepoHeadFromTx sto tx >>= toMPlus
liftIO $ print $ vcat (fmap formatRef (_repoHeadRefs rh))
pKey :: GitPerks m => Parser (GitCLI m ())
pKey = hsubparser ( command "show" (info pKeyShow (progDesc "show current key"))
<> command "update" (info pKeyUpdate (progDesc "update current key"))
)
<|> pKeyShow
pKeyShow :: GitPerks m => Parser (GitCLI m ())
pKeyShow = do
full <- flag False True (long "full" <> help "show full key info")
pure do
sto <- asks _storage
void $ runMaybeT do
tx <- withState do
selectMaxAppliedTx >>= lift . toMPlus <&> fst
rh <- TX.readRepoHeadFromTx sto tx
>>= toMPlus
gkh <- toMPlus (_repoHeadGK0 rh)
if not full then do
liftIO $ print $ pretty gkh
else do
gk <- runExceptT (readGK0 sto gkh) >>= toMPlus
liftIO $ print $ ";; group key" <+> pretty gkh <> line <> line <> pretty gk
pKeyUpdate :: GitPerks m => Parser (GitCLI m ())
pKeyUpdate = do
rlog <- argument pRefLogId (metavar "REFLOG-KEY")
fn <- strArgument (metavar "GROUP-KEY-FILE")
pure do
gk <- loadGK0FromFile fn
`orDie` "can not load group key or invalid format"
sto <- asks _storage
gh <- writeAsMerkle sto (serialise gk) <&> HashRef
added <- withState $ runMaybeT do
(tx,_) <- lift selectMaxAppliedTx >>= toMPlus
lift do
insertNewGK0 rlog tx gh
commitAll
pure gh
case added of
Nothing -> liftIO $ putStrLn "not added" >> exitFailure
Just x -> liftIO $ print $ pretty x
main :: IO ()
main = do
(o, action) <- customExecParser (prefs showHelpOnError) $
O.info (liftA2 (,) globalOptions commands <**> helper)
( fullDesc
<> header "hbs2-git"
<> progDesc "hbs2-git"
)
runGitCLI o action

View File

@ -0,0 +1,204 @@
module Main where
import Prelude hiding (getLine)
import HBS2.Git.Client.Prelude
import HBS2.Git.Client.App
import HBS2.Git.Client.Import
import HBS2.Git.Client.Export
import HBS2.Git.Client.State
import HBS2.Git.Client.Progress
import HBS2.Git.Client.Config
import HBS2.Git.Data.RefLog
import HBS2.Git.Data.Tx qualified as TX
import HBS2.Git.Data.Tx (RepoHead(..))
import HBS2.System.Dir
import Control.Concurrent.STM qualified as STM
import System.Posix.Signals
import System.Environment
import System.IO (hPutStrLn)
import System.IO qualified as IO
import System.Exit qualified as Exit
import Data.ByteString.Char8 qualified as BS8
import Data.ByteString.Lazy.Char8 qualified as LBS8
import Data.Attoparsec.ByteString.Char8 hiding (try)
import Data.Attoparsec.ByteString.Char8 qualified as Atto
import Data.Maybe
import Data.HashMap.Strict qualified as HM
import Data.List qualified as L
import Text.InterpolatedString.Perl6 (qc)
import System.Exit hiding (die)
{- HLINT ignore "Use isEOF" -}
{- HLINT ignore "Use putStrLn" -}
done :: MonadIO m => m Bool
done = hIsEOF stdin
getLine :: MonadIO m => m String
getLine = liftIO IO.getLine
sendLine :: MonadIO m => String -> m ()
sendLine = liftIO . IO.putStrLn
die :: (MonadIO m, Pretty a) => a -> m b
die s = liftIO $ Exit.die (show $ pretty s)
parseURL :: String -> Maybe RefLogId
parseURL s = eitherToMaybe $ Atto.parseOnly p (BS8.pack s)
where
p = do
void $ string "hbs21://" <|> string "hbs2://"
Atto.takeWhile1 (`elem` getAlphabet)
<&> BS8.unpack
<&> fromStringMay @RefLogId
>>= maybe (fail "invalid reflog key") pure
parsePush :: String -> Maybe (Maybe GitRef, GitRef)
parsePush s = eitherToMaybe $ Atto.parseOnly p (BS8.pack s)
where
gitref = fromString @GitRef . BS8.unpack
p = do
a <- optional (Atto.takeWhile1 (/= ':')) <&> fmap gitref
char ':'
b <- Atto.takeWhile1 (const True) <&> gitref
pure (a,b)
data S =
Plain
| Push
deriving stock (Eq,Ord,Show,Enum)
{- HLINT ignore "Functor law" -}
main :: IO ()
main = do
hSetBuffering stdin LineBuffering
hSetBuffering stdout LineBuffering
void $ installHandler sigPIPE Ignore Nothing
args <- getArgs
(remote, puk) <- case args of
[s, u] ->
(s,) <$> pure (parseURL u)
`orDie` show ("invalid reflog" <+> pretty u)
_ -> die "bad args"
runGitCLI mempty $ do
env <- ask
flip runContT pure do
lift $ withGitEnv (env & set gitApplyHeads False) do
debug $ red "run" <+> pretty args
sto <- asks _storage
ip <- asks _progress
importRepoWait puk
`catch` (\(_ :: ImportRefLogNotFound) -> do
onProgress ip ImportAllDone
let url = headMay (catMaybes [ parseURL a | a <- args]) <&> AsBase58
pause @'Seconds 0.25
liftIO $ hFlush stderr
liftIO $ hPutDoc stderr $ ""
<> ul (yellow "Reflog" <+> pretty url <+> yellow "is not available yet.") <> line
<> "If you sure it's a new one -- make sure you've added the key to hbs2-keyman and then run"
<> line <> line
<> "hbs2-keyman update" <> line <> line
<> "git" <+> pretty hbs2Name <+> "export --new" <+> pretty url <> line <> line
<> "to init the reflog first." <> line
<> "Pushing to an existing reflog as a new one may cause unwanted data duplication." <> line
<> line
<> "Note: what ever pushed -- can not be unpushed" <> line
<> "If it's not a new reflog --- just wait until it became available"
liftIO exitFailure
)
void $ runExceptT do
tpush <- newTQueueIO -- @(GitRef, Maybe GitHash)
flip fix Plain $ \next s -> do
eof <- done
when eof $ pure ()
cmd <- ExceptT (try @_ @IOError (getLine <&> words))
debug $ "C:" <+> pretty cmd
case cmd of
[] | s == Plain -> do
onProgress ip (ImportSetQuiet True)
pure ()
[] | s == Push -> do
refs <- atomically (STM.flushTQueue tpush)
<&> HM.toList . HM.fromList
importRepoWait puk
export puk refs
sendLine ""
next Plain
["capabilities"] -> do
debug $ "send capabilities"
sendLine "push"
sendLine "fetch"
sendLine ""
next Plain
("list" : _) -> do
r' <- runMaybeT $ withState do
tx <- selectMaxAppliedTx >>= lift . toMPlus <&> fst
rh <- TX.readRepoHeadFromTx sto tx >>= lift . toMPlus
pure (_repoHeadRefs rh)
let r = fromMaybe mempty r'
forM_ (fmap (show . formatRef) r) sendLine
sendLine ""
next Plain
("push" : pargs : _ ) -> do
(fromRef, toRef) <- orThrowUser "can't parse push" (parsePush pargs)
r <- readProcess (setStderr closed $ shell [qc|git rev-parse {pretty $ fromRef}|])
<&> headDef "" . LBS8.words . view _2
<&> fromStringMay @GitHash . LBS8.unpack
let val = const r =<< fromRef
atomically $ writeTQueue tpush (toRef, val)
sendLine [qc|ok {pretty toRef}|]
next Push
_ -> next Plain
pure ()
`finally` liftIO do
hPutStrLn stdout "" >> hFlush stdout
-- notice $ red "BYE"
hPutStrLn stderr ""

View File

@ -0,0 +1,193 @@
module HBS2.Git.Client.App
( module HBS2.Git.Client.App
, module HBS2.Git.Client.App.Types
) where
import HBS2.Git.Client.Prelude hiding (info)
import HBS2.Git.Client.App.Types
import HBS2.Git.Client.Config
import HBS2.Git.Client.Progress
import HBS2.Git.Client.State
import HBS2.Git.Data.Tx
import HBS2.Git.Local.CLI
import HBS2.System.Dir
import Data.Maybe
import System.Environment
import System.IO (hPutStr)
import Data.Vector qualified as V
import Data.Vector ((!))
drawProgress :: MonadUnliftIO m => ProgressQ -> m ()
drawProgress (ProgressQ q) = do
let spin = V.fromList ["--","\\","|","/"]
let l = V.length spin
i <- newTVarIO 0
tl <- newTVarIO =<< getTimeCoarse
let updateSpinner = do
atomically $ modifyTVar i succ
let getSpinner = do
j <- readTVarIO i <&> (`mod` l)
pure $ spin ! j
let
limit :: MonadIO m => Timeout 'Seconds -> m () -> m ()
limit dt m = do
t0 <- readTVarIO tl
now <- getTimeCoarse
when (expired dt (now - t0)) do
atomically $ writeTVar tl now
m
let loop = do
flip fix False \next quiet -> do
let put s | quiet = pure ()
| otherwise = putStatus s
ev <- atomically $ readTQueue q
case ev of
ImportIdle -> do
next quiet
ImportSetQuiet qq -> do
put ""
next qq
ImportRefLogStart puk -> do
put ("wait reflog" <+> pretty (AsBase58 puk))
next quiet
ImportRefLogDone puk Nothing -> do
updateSpinner
c <- getSpinner
put ("wait reflog" <+> pretty (AsBase58 puk) <+> pretty c)
next quiet
ImportRefLogDone _ (Just h) -> do
put ("reflog value" <+> pretty h)
next quiet
ImportWaitTx h -> do
updateSpinner
c <- getSpinner
put ("wait tx data" <+> pretty h <+> pretty c)
next quiet
ImportScanTx h -> do
put ("scan tx" <+> pretty h)
next quiet
ImportApplyTx h -> do
put ("apply tx" <+> pretty h)
next quiet
ImportReadBundleChunk meta (Progress s _) -> do
let h = bundleHash meta
let e = if bundleEncrypted meta then yellow "@" else ""
limit 0.5 $ put $ "read pack" <+> e <> pretty h <+> pretty s
next quiet
ExportWriteObject (Progress s _) -> do
limit 0.5 $ put $ "write object" <+> pretty s
next quiet
ImportAllDone -> do
put "\n"
loop
`finally` do
putStatus ""
where
putStatus :: MonadUnliftIO m => Doc AnsiStyle -> m ()
putStatus s = do
liftIO $ hPutStr stderr $ toStringANSI $ "\r" <> fill 80 "" <> "\r" <> pretty (take 74 (toStringANSI s))
liftIO $ hFlush stderr
runGitCLI :: (GitPerks m) => [GitOption] -> GitCLI m a -> m a
runGitCLI o m = do
soname <- runExceptT getSocketName
>>= orThrowUser "no rpc socket"
flip runContT pure do
client <- lift $ race (pause @'Seconds 1) (newMessagingUnix False 1.0 soname)
>>= orThrowUser ("can't connect to" <+> pretty soname)
void $ ContT $ withAsync $ runMessagingUnix client
peerAPI <- makeServiceCaller @PeerAPI (fromString soname)
refLogAPI <- makeServiceCaller @RefLogAPI (fromString soname)
storageAPI <- makeServiceCaller @StorageAPI (fromString soname)
let endpoints = [ Endpoint @UNIX peerAPI
, Endpoint @UNIX refLogAPI
, Endpoint @UNIX storageAPI
]
void $ ContT $ withAsync $ liftIO $ runReaderT (runServiceClientMulti endpoints) client
conf <- lift $ readConfig True
git <- gitDir
>>= orThrowUser "git dir not set"
>>= canonicalizePath
q <- lift newProgressQ
let ip = AnyProgress q
cpath <- lift getConfigDir
progress <- ContT $ withAsync (drawProgress q)
env <- lift $ newGitEnv ip o git cpath conf peerAPI refLogAPI storageAPI
lift $ runReaderT setupLogging env
lift $ withGitEnv env (evolveDB >> m)
`finally` do
onProgress ip ImportAllDone
cancel progress
shutDownLogging
runDefault :: GitPerks m => GitCLI m ()
runDefault = do
pure ()
setupLogging :: (GitPerks m, HasGitOpts m) => m ()
setupLogging = do
traceEnv <- liftIO $ lookupEnv "HBS2TRACE" <&> isJust
setLogging @INFO defLog
setLogging @ERROR (logPrefix "" . toStderr)
setLogging @WARN (logPrefix "" . toStderr)
setLogging @NOTICE (logPrefix "" . toStderr)
dbg <- debugEnabled
when (dbg || traceEnv) do
setLogging @DEBUG (logPrefix "" . toStderr)
trc <- traceEnabled
when (trc || traceEnv) do
setLogging @TRACE (logPrefix "" . toStderr)
shutDownLogging :: MonadUnliftIO m => m ()
shutDownLogging = do
setLoggingOff @INFO
setLoggingOff @ERROR
setLoggingOff @WARN
setLoggingOff @NOTICE
setLoggingOff @DEBUG
setLoggingOff @TRACE

View File

@ -0,0 +1,141 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# Language UndecidableInstances #-}
module HBS2.Git.Client.App.Types
( module HBS2.Git.Client.App.Types
, module HBS2.Git.Client.App.Types.GitEnv
, module HBS2.Git.Local
, module Data.Config.Suckless
, module Control.Monad.Catch
) where
import HBS2.Git.Client.Prelude hiding (info)
import HBS2.Git.Client.Progress
import HBS2.Git.Local
import HBS2.Git.Client.App.Types.GitEnv
import HBS2.Git.Data.Tx
import HBS2.Git.Data.GK
import HBS2.KeyMan.Keys.Direct
import HBS2.Net.Auth.GroupKeySymm
import HBS2.Storage.Operations.ByteString
import HBS2.System.Dir
import Data.Config.Suckless
import Control.Monad.Catch (MonadThrow(..))
import DBPipe.SQLite
import Data.HashMap.Strict qualified as HM
import Data.Maybe
import Data.Word
type Epoch = Word64
data GitOption = GitTrace
| GitDebug
| GitExport ExportType
| GitEnc ExportEncryption
| GitDontApplyHeads
deriving stock (Eq,Ord)
newtype GitCLI m a = GitCLI { fromGitCLI :: ReaderT GitEnv m a }
deriving newtype ( Applicative
, Functor
, Monad
, MonadIO
, MonadUnliftIO
, MonadReader GitEnv
, MonadThrow
)
type GitPerks m = ( MonadUnliftIO m, MonadThrow m )
newGitEnv :: GitPerks m
=> AnyProgress
-> [GitOption]
-> FilePath
-> FilePath
-> Config
-> ServiceCaller PeerAPI UNIX
-> ServiceCaller RefLogAPI UNIX
-> ServiceCaller StorageAPI UNIX
-> m GitEnv
newGitEnv p opts path cpath conf peer reflog sto = do
let dbfile = cpath </> "state.db"
let dOpt = dbPipeOptsDef { dbLogger = \x -> debug ("state:" <+> pretty x) }
db <- newDBPipeEnv dOpt dbfile
cache <- newTVarIO mempty
pure $ GitEnv
traceOpt
debugOpt
applyHeadsOpt
exportType
exportEnc
path
cpath
conf
peer
reflog
(AnyStorage (StorageClient sto))
db
p
cache
where
traceOpt = GitTrace `elem` opts
debugOpt = GitDebug `elem` opts
applyHeadsOpt = GitDontApplyHeads `notElem` opts
-- FIXME: from-options
exportType = lastDef ExportInc [ t | GitExport t <- opts ]
exportEnc = lastDef ExportPublic [ t | GitEnc t <- opts ]
withGitEnv :: GitPerks m => GitEnv -> GitCLI m a -> m a
withGitEnv env m = runReaderT (fromGitCLI m) env
instance (GitPerks m, MonadReader GitEnv m) => GroupKeyOperations m where
-- FIXME: may-be-faster
loadKeyrings gkh = do
sto <- asks _storage
cache <- asks _keyringCache
let k = gkh
ke <- readTVarIO cache <&> HM.lookup k
case ke of
Just es -> pure es
Nothing -> do
rcpt <- fromMaybe mempty <$> runMaybeT do
runExceptT (readGK0 sto gkh)
>>= toMPlus
<&> HM.keys . recipients
es <- runKeymanClient $ do
loadKeyRingEntries rcpt
<&> fmap snd
atomically $ modifyTVar cache (HM.insert k es)
pure es
openGroupKey gk = runMaybeT do
ke' <- lift $ runKeymanClient do
loadKeyRingEntries (HM.keys $ recipients gk)
<&> headMay
(_, KeyringEntry{..}) <- toMPlus ke'
toMPlus $ lookupGroupKey _krSk _krPk gk
class HasGitOpts m where
debugEnabled :: m Bool
traceEnabled :: m Bool
instance MonadReader GitEnv m => HasGitOpts m where
debugEnabled = asks _gitDebugEnabled
traceEnabled = asks _gitTraceEnabled

View File

@ -0,0 +1,44 @@
{-# Language TemplateHaskell #-}
module HBS2.Git.Client.App.Types.GitEnv where
import HBS2.Git.Client.Prelude hiding (info)
import HBS2.Git.Client.Progress
import HBS2.Net.Auth.GroupKeySymm
import Data.Config.Suckless
import DBPipe.SQLite
import Data.HashMap.Strict (HashMap)
data ExportType = ExportNew
| ExportFork HashRef
| ExportInc
deriving stock (Eq,Ord,Generic,Show)
data ExportEncryption =
ExportPublic
| ExportPrivate FilePath
deriving stock (Eq,Ord,Generic,Show)
type Config = [Syntax C]
data GitEnv =
GitEnv
{ _gitTraceEnabled :: Bool
, _gitDebugEnabled :: Bool
, _gitApplyHeads :: Bool
, _gitExportType :: ExportType
, _gitExportEnc :: ExportEncryption
, _gitPath :: FilePath
, _configPath :: FilePath
, _config :: Config
, _peerAPI :: ServiceCaller PeerAPI UNIX
, _refLogAPI :: ServiceCaller RefLogAPI UNIX
, _storage :: AnyStorage -- ServiceCaller StorageAPI UNIX
, _db :: DBPipeEnv
, _progress :: AnyProgress
, _keyringCache :: TVar (HashMap HashRef [KeyringEntry HBS2Basic])
}
makeLenses 'GitEnv

View File

@ -0,0 +1,89 @@
module HBS2.Git.Client.Config (getConfigDir, readConfig, getManifest, hbs2Name) where
import HBS2.Git.Client.Prelude
import HBS2.Git.Client.App.Types
import HBS2.System.Dir
import HBS2.Git.Local.CLI
import Data.List qualified as L
import Data.Text qualified as Text
import Data.Either
import Text.InterpolatedString.Perl6 (qc)
data ConfigDirNotFound = ConfigDirNotFound
deriving stock (Show,Typeable,Generic)
instance HasErrorStatus ConfigDirNotFound where
getStatus = const Failed
instance Exception ConfigDirNotFound
hbs2Name :: String
hbs2Name = "hbs21"
getConfigDir :: GitPerks m => m FilePath
getConfigDir = do
git <- gitDir >>= orThrow ConfigDirNotFound
let p = splitDirectories git & reverse
if headMay p == Just ".git" then
pure $ joinPath $ reverse (".hbs2-git" : drop 1 p)
else do
pure $ git </> ".hbs2-git"
getManifest :: GitPerks m => m (Text, Text, Maybe Text)
getManifest = do
dir <- getConfigDir
let mf = dir </> "manifest"
let defname = takeFileName (takeDirectory dir) & Text.pack
let defbrief = "n/a"
content <- liftIO (try @_ @IOException $ readFile mf)
<&> fromRight ""
let txt = if L.null content then Nothing else Just (Text.pack content)
-- FIXME: size-hardcode
let header = lines (take 1024 content)
& takeWhile ( not . L.null )
& unlines
& parseTop
& fromRight mempty
let name = lastDef defname [ n | ListVal [ SymbolVal "name:", LitStrVal n ] <- header ]
let brief = lastDef defbrief [ n | ListVal [ SymbolVal "brief:", LitStrVal n ] <- header ]
pure (name,brief,txt)
readConfig :: (GitPerks m) => Bool -> m Config
readConfig canTouch = do
{- HLINT ignore "Functor law" -}
confPath <- getConfigDir
let confRoot = confPath </> "config"
when canTouch do
here <- doesPathExist confRoot
unless here do
mkdir confPath
liftIO $ writeFile confRoot defConf
try @_ @SomeException (liftIO (readFile confRoot))
<&> fromRight mempty
<&> parseTop
<&> fromRight mempty
defConf :: String
defConf = [qc|;; hbs2-git config file
; those branches will be replicated by default
export include "refs/heads/master"
export include "refs/heads/main"
export exclude "refs/heads/*"
export tags
|]

View File

@ -0,0 +1,293 @@
module HBS2.Git.Client.Export (export) where
import HBS2.Git.Client.Prelude hiding (info)
import HBS2.Git.Client.App.Types
import HBS2.Git.Client.Config
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
import HBS2.Git.Data.GK
import HBS2.Git.Local.CLI
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
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
<&> filterPat incl excl
<&> HashMap.fromList
<&> HashMap.filterWithKey (\k _ -> not (HashSet.member k deleted))
<&> 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
gkf <- headMay [ f | ExportPrivate f <- [enc] ] & toMPlus
gk <- loadGK0FromFile gkf >>= toMPlus
epoch <- getEpoch
writeAsMerkle sto (serialise gk) <&> HashRef <&> (,epoch)
export :: (GitPerks m, MonadReader GitEnv m, GroupKeyOperations m)
=> RefLogId
-> [(GitRef,Maybe GitHash)]
-> m ()
export puk refs = do
subscribeRefLog puk
git <- asks _gitPath
sto <- asks _storage
new <- asks _gitExportType <&> (== ExportNew)
reflog <- asks _refLogAPI
ip <- asks _progress
myrefs <- refsForExport refs
let myrefsKey = L.sortOn fst myrefs & serialise & hashObject @HbSync & HashRef
flip runContT pure do
callCC \exit -> do
tx0 <- getLastAppliedTx
rh0 <- runMaybeT ( toMPlus tx0 >>= readRepoHeadFromTx sto >>= toMPlus )
(name,brief,mf) <- lift getManifest
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)
when (null objs && not new && oldRefs == myrefs) 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 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
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

View File

@ -0,0 +1,292 @@
module HBS2.Git.Client.Import where
import HBS2.Git.Client.Prelude hiding (info)
import HBS2.Git.Client.App.Types
import HBS2.Git.Client.Config
import HBS2.Git.Client.State
import HBS2.Git.Client.RefLog
import HBS2.Git.Client.Progress
import HBS2.Git.Data.RefLog
import HBS2.Git.Data.Tx
import Data.ByteString.Lazy qualified as LBS
import Text.InterpolatedString.Perl6 (qc)
import Streaming.Prelude qualified as S
import System.IO (hPrint)
import System.Environment
import System.Exit
data ImportRefLogNotFound = ImportRefLogNotFound
deriving stock (Typeable,Show)
instance Exception ImportRefLogNotFound
data ImportTxError =
ImportTxReadError HashRef
| ImportOpError OperationError
| ImportUnbundleError HashRef
| ImportMissed HashRef
deriving stock (Typeable)
instance Show ImportTxError where
show (ImportTxReadError h) = [qc|ImportTxError {pretty h}|]
show (ImportOpError o) = show o
show (ImportUnbundleError h) = [qc|ImportUnbundleError {pretty h}|]
show (ImportMissed h) = [qc|ImportMissed {pretty h}|]
instance Exception ImportTxError
data IState =
IWaitRefLog Int
| IScanRefLog HashRef
| IApplyTx HashRef
| IExit
importRepoWait :: (GitPerks m, MonadReader GitEnv m)
=> RefLogId
-> m ()
importRepoWait puk = do
env <- ask
subscribeRefLog puk
ip <- asks _progress
flip fix (IWaitRefLog 20) $ \next -> \case
IWaitRefLog w | w <= 0 -> do
throwIO ImportRefLogNotFound
IWaitRefLog w -> do
onProgress ip (ImportRefLogStart puk)
try @_ @SomeException (getRefLogMerkle puk) >>= \case
Left _ -> do
onProgress ip (ImportRefLogDone puk Nothing)
pause @'Seconds 2
next (IWaitRefLog (pred w))
Right Nothing -> do
onProgress ip (ImportRefLogDone puk Nothing)
pause @'Seconds 2
next (IWaitRefLog (pred w))
Right (Just h) -> do
onProgress ip (ImportRefLogDone puk (Just h))
next (IScanRefLog h)
IScanRefLog h -> do
scanRefLog puk h
withState (selectMaxSeqTxNotDone puk) >>= \case
Just tx -> next (IApplyTx tx)
Nothing -> do
hasAnyTx <- withState existsAnyTxDone
if hasAnyTx then -- existing repo, is' a fetch
next IExit
else do
void $ race (pause @'Seconds 10) do
forever do
onProgress ip (ImportWaitTx h)
pause @'Seconds 0.25
next (IScanRefLog h)
IApplyTx h -> do
onProgress ip (ImportApplyTx h)
r <- runExceptT (applyTx h)
case r of
Left MissedBlockError -> do
next =<< repeatOrExit
Left IncompleteData -> do
next =<< repeatOrExit
Left e -> do
err (line <> red (viaShow e))
throwIO (userError "tx apply / state read error")
Right{} -> next IExit
IExit -> do
onProgress ip (ImportSetQuiet True)
onProgress ip ImportAllDone
where
repeatOrExit = do
hasAnyTx <- withState existsAnyTxDone
if hasAnyTx then do
pure IExit
else do
pause @'Seconds 2
pure (IWaitRefLog 5)
scanRefLog :: (GitPerks m, MonadReader GitEnv m)
=> RefLogId
-> HashRef
-> m ()
scanRefLog puk rv = do
sto <- asks _storage
ip <- asks _progress
env <- ask
txs <- S.toList_ $ do
walkMerkle @[HashRef] (fromHashRef rv) (getBlock sto) $ \case
Left he -> do
err $ red "missed block" <+> pretty he
Right hxs -> do
for_ hxs $ \htx -> do
here <- lift (withState (existsTx htx))
unless here (S.yield htx)
tx <- liftIO $ S.toList_ $ do
for_ txs $ \tx -> do
onProgress ip (ImportScanTx tx)
runExceptT (readTx sto tx <&> (tx,))
>>= either (const none) S.yield
withState $ transactional do
for_ tx $ \(th,(n,rhh,rh,bundleh)) -> do
-- notice $ red "TX" <+> pretty th <+> pretty n
insertTx puk th n rhh bundleh
applyTx :: (GitPerks m, MonadReader GitEnv m, MonadError OperationError m)
=> HashRef
-> m ()
applyTx h = do
sto <- asks _storage
(n,rhh,r,bunh) <- readTx sto h
bundles <- readBundleRefs sto bunh
>>= orThrowError IncompleteData
trace $ red "applyTx" <+> pretty h <+> pretty h <+> pretty bundles
withState $ transactional do
applyBundles r bundles
app <- lift $ asks (view gitApplyHeads)
when app do
lift $ applyHeads r
insertTxDone h
where
applyHeads rh = do
let refs = _repoHeadRefs rh
withGitFastImport $ \ps -> do
let psin = getStdin ps
for_ refs $ \(r,v) -> do
unless (r == GitRef "HEAD") do
liftIO $ hPrint psin $
"reset" <+> pretty r <> line <> "from" <+> pretty v <> line
hClose psin
code <- waitExitCode ps
trace $ red "git fast-import status" <+> viaShow code
pure ()
applyBundles r bundles = do
env <- lift ask
sto <- lift $ asks _storage
ip <- lift $ asks _progress
-- withState $ do
for_ (zip [0..] bundles) $ \(n,bu) -> do
insertTxBundle h n bu
here <- existsBundleDone bu
unless here do
BundleWithMeta meta bytes <- lift (runExceptT $ readBundle sto r bu)
>>= orThrow (ImportUnbundleError bu)
(_,_,idx,lbs) <- unpackPackMay bytes
& orThrow (ImportUnbundleError bu)
trace $ red "reading bundle" <+> pretty bu -- <+> pretty (LBS.length lbs)
for_ idx $ \i -> do
insertBundleObject bu i
let chunks = LBS.toChunks lbs
void $ liftIO $ withGitEnv env $ withGitUnpack $ \p -> do
let pstdin = getStdin p
for_ (zip [1..] chunks) $ \(i,chu) -> do
onProgress ip (ImportReadBundleChunk meta (Progress i Nothing))
liftIO $ LBS.hPutStr pstdin (LBS.fromStrict chu)
hFlush pstdin >> hClose pstdin
code <- waitExitCode p
trace $ "unpack objects done:" <+> viaShow code
insertBundleDone bu
withGitFastImport :: (MonadUnliftIO m, MonadReader GitEnv m)
=> (Process Handle Handle () -> m a)
-> m ()
withGitFastImport action = do
fp <- asks _gitPath
let cmd = "git"
let args = ["--git-dir", fp, "fast-import"]
-- let config = setStdin createPipe $ setStdout createPipe $ setStderr closed $ proc cmd args
trc <- asks traceEnabled >>= \case
True -> pure id
False -> pure $ setStdout closed . setStderr closed
let pconfig = setStdin createPipe $ setStdout createPipe $ trc $ proc cmd args
p <- startProcess pconfig
void $ action p
stopProcess p
withGitUnpack :: (MonadUnliftIO m, MonadReader GitEnv m)
=> (Process Handle Handle () -> m a) -> m a
withGitUnpack action = do
fp <- asks _gitPath
let cmd = "git"
let args = ["--git-dir", fp, "unpack-objects", "-q"]
trc <- asks traceEnabled >>= \case
True -> pure id
False -> pure $ setStdout closed . setStderr closed
let pconfig = setStdin createPipe $ setStdout createPipe $ trc $ proc cmd args
p <- startProcess pconfig
action p
gitPrune :: (MonadUnliftIO m, MonadReader GitEnv m)
=> m ()
gitPrune = do
fp <- asks _gitPath
let cmd = [qc|git --git-dir={fp} prune|]
runProcess_ (shell cmd & setStderr closed & setStdout closed)
pure ()

View File

@ -0,0 +1,88 @@
module HBS2.Git.Client.Prelude
( module HBS2.Prelude.Plated
, module HBS2.Base58
, module HBS2.Clock
, module HBS2.Hash
, module HBS2.Data.Types.Refs
, module HBS2.Net.Auth.Credentials
, module HBS2.Merkle
, module HBS2.Storage
, module HBS2.Net.Messaging.Unix
, module HBS2.OrDie
, module HBS2.Misc.PrettyStuff
, module HBS2.System.Logger.Simple.ANSI
-- peer
, module HBS2.Net.Proto.Service
, module HBS2.Peer.RPC.API.Peer
, module HBS2.Peer.RPC.API.RefLog
, module HBS2.Peer.RPC.API.Storage
, module HBS2.Peer.RPC.Client.StorageClient
, module Control.Applicative
, module Control.Monad.Reader
, module Control.Monad.Trans.Cont
, module Control.Monad.Trans.Maybe
, module System.Process.Typed
, module Control.Monad.Except
, module Lens.Micro.Platform
, module UnliftIO
, getSocketName
, formatRef
, deserialiseOrFail
) where
import HBS2.Prelude.Plated hiding (at)
import HBS2.Base58
import HBS2.Clock
import HBS2.Peer.Proto
import HBS2.Hash
import HBS2.Data.Types.Refs
import HBS2.Net.Auth.Credentials
import HBS2.Merkle
import HBS2.Storage
import HBS2.OrDie
import HBS2.Misc.PrettyStuff
import HBS2.System.Logger.Simple.ANSI
import HBS2.Net.Messaging.Unix
import HBS2.Net.Proto.Service
import HBS2.Peer.RPC.API.Peer
import HBS2.Peer.RPC.API.RefLog
import HBS2.Peer.RPC.API.Storage
import HBS2.Peer.RPC.Client.StorageClient
import HBS2.Peer.CLI.Detect
import Control.Applicative
import Control.Monad.Trans.Cont
import Control.Monad.Reader
import Control.Monad.Except
import Control.Exception
import Control.Monad.Trans.Maybe
import UnliftIO
import System.Process.Typed
import Lens.Micro.Platform
import Codec.Serialise
data RPCNotFoundError = RPCNotFoundError
deriving stock (Show,Typeable)
instance Exception RPCNotFoundError
instance HasErrorStatus RPCNotFoundError where
getStatus = const Failed
getSocketName :: forall m . (MonadUnliftIO m, MonadError RPCNotFoundError m) => m FilePath
getSocketName = do
detectRPC >>= maybe (throwError RPCNotFoundError) pure
formatRef :: (Pretty a1, Pretty a2) => (a1, a2) -> Doc ann
formatRef (r,h) = pretty h <+> pretty r

View File

@ -0,0 +1,52 @@
{-# Language TemplateHaskell #-}
{-# Language AllowAmbiguousTypes #-}
module HBS2.Git.Client.Progress where
import HBS2.Git.Client.Prelude
import HBS2.Git.Data.RefLog
import HBS2.Git.Data.Tx
data Progress a =
Progress
{ _progressState :: a
, _progressTotal :: Maybe a
}
deriving (Eq,Generic)
makeLenses 'Progress
class HasProgress a where
onProgress :: MonadIO m => a -> ProgressEvent -> m ()
data ProgressEvent =
ImportIdle
| ImportRefLogStart RefLogId
| ImportRefLogDone RefLogId (Maybe HashRef)
| ImportWaitTx HashRef
| ImportScanTx HashRef
| ImportApplyTx HashRef
| ImportReadBundleChunk BundleMeta (Progress Int)
| ImportSetQuiet Bool
| ImportAllDone
| ExportWriteObject (Progress Int)
data AnyProgress = forall a . HasProgress a => AnyProgress a
instance HasProgress AnyProgress where
onProgress (AnyProgress e) = onProgress e
instance HasProgress () where
onProgress _ _ = pure ()
newtype ProgressQ = ProgressQ (TQueue ProgressEvent)
instance HasProgress ProgressQ where
onProgress (ProgressQ q) ev = atomically (writeTQueue q ev)
newProgressQ :: MonadUnliftIO m => m ProgressQ
newProgressQ = ProgressQ <$> newTQueueIO

View File

@ -0,0 +1,37 @@
module HBS2.Git.Client.RefLog where
import HBS2.Git.Client.Prelude
import HBS2.Git.Client.App.Types
import HBS2.Git.Data.RefLog
data RefLogRequestTimeout = RefLogRequestTimeout
deriving (Show,Typeable)
data RefLogRequestError = RefLogRequestError
deriving (Show,Typeable)
instance Exception RefLogRequestTimeout
instance Exception RefLogRequestError
subscribeRefLog :: (GitPerks m, MonadReader GitEnv m) => RefLogId -> m ()
subscribeRefLog puk = do
api <- asks _peerAPI
void $ callService @RpcPollAdd api (puk, "reflog", 13)
getRefLogMerkle :: (GitPerks m, MonadReader GitEnv m) => RefLogId -> m (Maybe HashRef)
getRefLogMerkle puk = do
api <- asks _refLogAPI
void $ race (pause @'Seconds 1) (callService @RpcRefLogFetch api puk)
>>= orThrow RefLogRequestTimeout
>>= orThrow RefLogRequestError
race (pause @'Seconds 1) (callService @RpcRefLogGet api puk)
>>= orThrow RefLogRequestTimeout
>>= orThrow RefLogRequestError

View File

@ -0,0 +1,348 @@
{-# Language UndecidableInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module HBS2.Git.Client.State
( module HBS2.Git.Client.State
, transactional
, commitAll
) where
import HBS2.Git.Client.Prelude
import HBS2.Git.Client.App.Types
import HBS2.Git.Client.Config
import HBS2.Git.Data.RefLog
import DBPipe.SQLite
import Data.Maybe
import Data.List qualified as List
import Text.InterpolatedString.Perl6 (qc)
newtype Base58Field a = Base58Field { fromBase58Field :: a }
deriving stock (Eq,Ord,Generic)
instance Pretty (AsBase58 a) => ToField (Base58Field a) where
toField (Base58Field x) = toField @String (show $ pretty (AsBase58 x))
instance IsString a => FromField (Base58Field a) where
fromField = fmap (Base58Field . fromString) . fromField @String
instance ToField HashRef where
toField h = toField @String (show $ pretty h)
instance FromField HashRef where
fromField = fmap fromString . fromField @String
instance ToField GitHash where
toField h = toField (show $ pretty h)
instance ToField GitRef where
toField h = toField (show $ pretty h)
instance FromField GitRef where
fromField = fmap fromString . fromField @String
instance FromField GitHash where
fromField = fmap fromString . fromField @String
createStateDir :: (GitPerks m, MonadReader GitEnv m) => m ()
createStateDir = do
void $ readConfig True
initState :: (GitPerks m, MonadReader GitEnv m) => m ()
initState = do
createStateDir
evolveDB
class WithState m a where
withState :: DBPipeM m a -> m a
instance (MonadIO m, MonadReader GitEnv m) => WithState m a where
withState action = do
env <- asks _db
withDB env action
evolveDB :: (GitPerks m, MonadReader GitEnv m) => m ()
evolveDB = withState do
createTxTable
createTxDoneTable
createTxBundleTable
createBundleDoneTable
createBundleKeyTable
createBundleObjectTable
createNewGK0Table
commitAll
createTxTable :: MonadIO m => DBPipeM m ()
createTxTable = do
ddl [qc|
create table if not exists tx
( reflog text not null
, tx text not null
, seq int not null
, head text not null
, bundle text not null
, primary key (reflog,tx)
)
|]
ddl [qc|
CREATE INDEX IF NOT EXISTS idx_tx_seq ON tx(seq)
|]
createTxDoneTable :: MonadIO m => DBPipeM m ()
createTxDoneTable = do
ddl [qc|
create table if not exists txdone
( tx text not null primary key
)
|]
createBundleDoneTable :: MonadIO m => DBPipeM m ()
createBundleDoneTable = do
ddl [qc|
create table if not exists bundledone
( hash text primary key
)
|]
createBundleKeyTable :: MonadIO m => DBPipeM m ()
createBundleKeyTable = do
ddl [qc|
create table if not exists bundlekey
( reflog text not null
, key text not null
, bundle text not null
, primary key (reflog, key)
)
|]
createTxBundleTable :: MonadIO m => DBPipeM m ()
createTxBundleTable = do
ddl [qc|
create table if not exists txbundle
( tx text not null
, num integer not null
, bundle text not null
, primary key (tx, num)
)
|]
createBundleObjectTable :: MonadIO m => DBPipeM m ()
createBundleObjectTable = do
ddl [qc|
create table if not exists bundleobject
( bundle text not null
, object text not null
, primary key (bundle, object)
)
|]
createNewGK0Table :: MonadIO m => DBPipeM m ()
createNewGK0Table = do
ddl [qc|
create table if not exists newgk0
( reflog text not null
, tx text not null
, ts int not null default (strftime('%s','now'))
, gk0 text not null
, primary key (reflog,tx)
)
|]
existsTx :: MonadIO m => HashRef -> DBPipeM m Bool
existsTx txHash = do
select @(Only Bool) [qc|
SELECT true FROM tx WHERE tx = ? LIMIT 1
|] (Only txHash)
<&> not . List.null
insertTx :: MonadIO m
=> RefLogId
-> HashRef
-> Integer
-> HashRef
-> HashRef
-> DBPipeM m ()
insertTx puk tx sn h bundle = do
insert [qc|
insert into tx (reflog,tx,seq,head,bundle)
values (?,?,?,?,?)
on conflict (reflog,tx) do nothing
|] (Base58Field puk,tx,sn,h,bundle)
selectTxForRefLog :: MonadIO m
=> RefLogId
-> HashRef
-> DBPipeM m (Maybe (HashRef, Epoch))
selectTxForRefLog puk tx = do
select [qc|
select head,seq
from tx where reflog = ? and tx = ?
limit 1
|] (Base58Field puk, tx) <&> listToMaybe
selectTxHead :: MonadIO m => HashRef -> DBPipeM m (Maybe HashRef)
selectTxHead txHash = do
result <- select [qc|
select head from tx where TX = ? limit 1
|] (Only txHash)
pure $ listToMaybe $ fmap fromOnly result
selectMaxTxSeq :: MonadIO m => RefLogId -> DBPipeM m Integer
selectMaxTxSeq puk = do
select [qc|
select max(seq) as seq from tx where reflog = ?
|] (Only (Base58Field puk))
<&> maybe 0 fromOnly . listToMaybe
insertTxDone :: MonadIO m => HashRef -> DBPipeM m ()
insertTxDone txHash = do
insert [qc|
INSERT INTO txdone (tx) VALUES (?)
ON CONFLICT (tx) DO NOTHING
|] (Only txHash)
existsTxDone :: MonadIO m => HashRef -> DBPipeM m Bool
existsTxDone txHash = do
select @(Only Bool) [qc|
SELECT true FROM txdone WHERE tx = ? LIMIT 1
|] (Only txHash)
<&> not . null
existsAnyTxDone :: MonadIO m => DBPipeM m Bool
existsAnyTxDone = do
select_ @_ @(Only (Maybe Bool)) [qc|
SELECT true FROM txdone LIMIT 1
|] <&> not . null
selectMaxSeqTxNotDone :: MonadIO m => RefLogId -> DBPipeM m (Maybe HashRef)
selectMaxSeqTxNotDone puk = do
select [qc|
WITH MaxDoneSeq AS (
SELECT MAX(tx.seq) as maxSeq
FROM tx
JOIN txdone ON tx.tx = txdone.tx
WHERE tx.reflog = ?
),
FilteredTx AS (
SELECT tx.tx, tx.seq
FROM tx
LEFT JOIN txdone ON tx.tx = txdone.tx
WHERE tx.reflog = ? AND txdone.tx IS NULL
)
SELECT ft.tx FROM FilteredTx ft
JOIN MaxDoneSeq mds ON ft.seq > COALESCE(mds.maxSeq, 0)
ORDER BY ft.seq DESC
LIMIT 1
|] (Base58Field puk, Base58Field puk)
<&> listToMaybe . fmap fromOnly
selectMaxAppliedTx :: MonadIO m => DBPipeM m (Maybe (HashRef, Integer))
selectMaxAppliedTx = do
select [qc|
SELECT t.tx, t.seq FROM txdone d JOIN tx t ON d.tx = t.tx ORDER BY t.seq DESC LIMIT 1
|] ()
<&> listToMaybe
insertBundleDone :: MonadIO m => HashRef -> DBPipeM m ()
insertBundleDone hashRef = do
insert [qc|
INSERT INTO bundledone (hash) VALUES (?)
ON CONFLICT (hash) DO NOTHING
|] (Only hashRef)
existsBundleDone :: MonadIO m => HashRef -> DBPipeM m Bool
existsBundleDone hashRef = do
select @(Only Bool) [qc|
SELECT true FROM bundledone WHERE hash = ? LIMIT 1
|] (Only hashRef)
<&> not . null
insertBundleKey :: MonadIO m => RefLogId -> HashRef -> HashRef -> DBPipeM m ()
insertBundleKey reflogId keyHash bundleHash = do
insert [qc|
INSERT INTO bundlekey (reflog, key, bundle) VALUES (?, ?, ?)
ON CONFLICT (reflog,key) DO NOTHING
|] (Base58Field reflogId, keyHash, bundleHash)
selectBundleByKey :: MonadIO m => RefLogId -> HashRef -> DBPipeM m (Maybe HashRef)
selectBundleByKey reflogId keyHash = do
select [qc|
SELECT bundle FROM bundlekey WHERE reflog = ? AND key = ? LIMIT 1
|] (Base58Field reflogId, keyHash)
<&> listToMaybe . fmap fromOnly
insertTxBundle :: MonadIO m => HashRef -> Int -> HashRef -> DBPipeM m ()
insertTxBundle tx num bundleHash = do
insert [qc|
INSERT INTO txbundle (tx, num, bundle) VALUES (?, ?, ?)
ON CONFLICT (tx, num) DO UPDATE SET bundle = EXCLUDED.bundle
|] (tx, num, bundleHash)
insertBundleObject :: MonadIO m => HashRef -> GitHash -> DBPipeM m ()
insertBundleObject bundle object = do
insert [qc|
insert into bundleobject (bundle, object) values (?, ?)
on conflict (bundle, object) do nothing
|] (bundle, object)
selectBundleObjects :: MonadIO m => HashRef -> DBPipeM m [GitHash]
selectBundleObjects bundle = do
select [qc|
select object from bundleobject where bundle = ?
|] (Only bundle)
<&> fmap fromOnly
selectObjectsForTx:: MonadIO m => HashRef -> DBPipeM m [GitHash]
selectObjectsForTx txHash = do
select [qc|
select distinct bundleobject.object
from txbundle
join bundleobject on txbundle.bundle = bundleobject.bundle
where txbundle.tx = ?
|] (Only txHash) <&> fmap fromOnly
isObjectInTx :: MonadIO m => HashRef -> GitHash -> DBPipeM m Bool
isObjectInTx txHash objectHash = do
result <- select @(Only Int) [qc|
select 1
from txbundle
join bundleobject on txbundle.bundle = bundleobject.bundle
where txbundle.tx = ? and bundleobject.object = ?
limit 1
|] (txHash, objectHash)
pure $ not (null result)
insertNewGK0 :: MonadIO m => RefLogId -> HashRef -> HashRef -> DBPipeM m ()
insertNewGK0 reflog tx gk0 = do
insert [qc|
insert into newgk0 (reflog, tx, gk0) values (?, ?, ?)
on conflict (reflog,tx) do update set gk0 = excluded.gk0
|] (Base58Field reflog, tx, gk0)
selectNewGK0 :: MonadIO m => RefLogId -> DBPipeM m (Maybe (HashRef,Epoch))
selectNewGK0 reflog = do
select [qc|
select gk0, ts
from newgk0 g
where g.reflog = ?
order by ts desc
limit 1
|] (Only (Base58Field reflog)) <&> listToMaybe

View File

@ -0,0 +1,26 @@
module HBS2.Git.Data.GK where
import HBS2.Git.Client.Prelude
import HBS2.Net.Auth.GroupKeySymm
import HBS2.Storage.Operations.ByteString
import Data.ByteString.Lazy qualified as LBS
type GK0 = GroupKey 'Symm HBS2Basic
readGK0 :: (MonadIO m, MonadError OperationError m) => AnyStorage -> HashRef -> m GK0
readGK0 sto h = do
runExceptT (readFromMerkle sto (SimpleKey (fromHashRef h)))
>>= orThrowError MissedBlockError
<&> deserialiseOrFail @GK0
>>= orThrowError UnsupportedFormat
loadGK0FromFile :: MonadIO m => FilePath -> m (Maybe GK0)
loadGK0FromFile fp = runMaybeT do
content <- liftIO (try @_ @IOError (LBS.readFile fp))
>>= toMPlus
toMPlus $ parseGroupKey @HBS2Basic (AsGroupKeyFile content)

View File

@ -0,0 +1,7 @@
module HBS2.Git.Data.RefLog where
import HBS2.Git.Client.Prelude
type RefLogId = PubKey 'Sign HBS2Basic

View File

@ -0,0 +1,383 @@
module HBS2.Git.Data.Tx
( module HBS2.Git.Data.Tx
, OperationError(..)
) where
import HBS2.Git.Client.Prelude
import HBS2.Git.Data.RefLog
import HBS2.Defaults
import HBS2.Data.Detect
import HBS2.KeyMan.Keys.Direct
import HBS2.Peer.Proto
import HBS2.Net.Auth.GroupKeySymm
import HBS2.Net.Auth.Credentials
import HBS2.Storage.Operations.ByteString
import HBS2.Storage.Operations.Missed
import HBS2.Git.Data.GK
import HBS2.Git.Local
import Data.Maybe
import Data.Either
import Data.Word
import Data.ByteString.Lazy qualified as LBS
import Data.ByteString (ByteString)
import Streaming.Prelude qualified as S
import Data.Binary.Get
import Data.ByteArray.Hash (SipHash(..), SipKey(..))
import Data.ByteArray.Hash qualified as BA
import Data.HashMap.Strict qualified as HM
type Rank = Integer
type LBS = LBS.ByteString
type RepoTx = RefLogUpdate L4Proto
data RepoHeadType = RepoHeadType1
deriving stock (Enum,Generic)
data RepoHeadExt = RepoHeadExt
deriving stock Generic
data RepoHead =
RepoHeadSimple
{ _repoHeadType :: RepoHeadType
, _repoHeadTime :: Word64
, _repoHeadGK0 :: Maybe HashRef
, _repoHeadName :: Text
, _repoHeadBrief :: Text
, _repoManifest :: Maybe Text
, _repoHeadRefs :: [(GitRef, GitHash)]
, _repoHeadExt :: [RepoHeadExt]
}
deriving stock (Generic)
instance Serialise RepoHeadType
instance Serialise RepoHeadExt
instance Serialise RepoHead
data TxKeyringNotFound = TxKeyringNotFound
deriving stock (Show, Typeable, Generic)
instance Exception TxKeyringNotFound
class GroupKeyOperations m where
openGroupKey :: GK0 -> m (Maybe GroupSecret)
loadKeyrings :: HashRef -> m [KeyringEntry HBS2Basic]
makeRepoHeadSimple :: MonadIO m
=> Text
-> Text
-> Maybe Text
-> Maybe HashRef
-> [(GitRef, GitHash)]
-> m RepoHead
makeRepoHeadSimple name brief manifest gk refs = do
t <- getEpoch
pure $ RepoHeadSimple RepoHeadType1 t gk name brief manifest refs mempty
writeRepoHead :: MonadUnliftIO m => AnyStorage -> RepoHead -> m HashRef
writeRepoHead sto rh = writeAsMerkle sto (serialise rh) <&> HashRef
makeTx :: (MonadUnliftIO m, GroupKeyOperations m)
=> AnyStorage
-> Bool -- ^ rewrite bundle merkle tree with new gk0
-> Rank -- ^ tx rank
-> RefLogId
-> RepoHead
-> [HashRef]
-> [LBS]
-> m RepoTx
makeTx sto rewrite r puk rh prev lbss = do
let rfk = RefLogKey @HBS2Basic puk
creds <- liftIO ( runKeymanClient $ loadCredentials puk )
>>= orThrow TxKeyringNotFound
let pubk = view peerSignPk creds
let privk = view peerSignSk creds
-- FIXME: delete-on-fail
headRef <- writeRepoHead sto rh
writeEnv <- newWriteBundleEnv sto rh
cRefs <- for lbss (writeBundle writeEnv)
let newBundles0 = prev <> cRefs
newBundles <- do
if not rewrite then do
pure newBundles0
else do
for newBundles0 \bh -> do
blk <- getBlock sto (fromHashRef bh)
>>= orThrow StorageError
case tryDetect (fromHashRef bh) blk of
Merkle{} -> do
bs <- runExceptT (readFromMerkle sto (SimpleKey (fromHashRef bh)))
>>= either throwIO pure
trace $ "encrypt existed block" <+> pretty bh
writeBundle writeEnv bs
MerkleAnn ann@(MTreeAnn {_mtaCrypt = EncryptGroupNaClSymm2 o gkh nonce}) -> do
gk <- runExceptT (readGK0 sto (HashRef gkh))
>>= orThrow (GroupKeyNotFound 4)
gks <- openGroupKey gk
>>= orThrow (GroupKeyNotFound 5)
debug $ "update GK0 for existed block" <+> pretty bh
let rcpt = HM.keys (recipients (wbeGk0 writeEnv))
gk1 <- generateGroupKey @HBS2Basic (Just gks) rcpt
gk1h <- writeAsMerkle sto (serialise gk1)
let newCrypt = EncryptGroupNaClSymm2 o gk1h nonce
let newTreeBlock = ann { _mtaCrypt = newCrypt }
newTree <- enqueueBlock sto (serialise newTreeBlock)
>>= orThrow StorageError
pure (HashRef newTree)
_ -> throwIO UnsupportedFormat
let pt = toPTree (MaxSize defHashListChunk) (MaxNum 256) newBundles
me <- makeMerkle 0 pt $ \(_,_,bss) -> do
void $ putBlock sto bss
let meRef = HashRef me
-- TODO: post-real-rank-for-tx
let tx = SequentialRef r (AnnotatedHashRef (Just headRef) meRef)
& serialise
& LBS.toStrict
makeRefLogUpdate @L4Proto @HBS2Basic pubk privk tx
unpackTx :: MonadIO m
=> RefLogUpdate L4Proto
-> m (Integer, HashRef, HashRef)
unpackTx tx = do
sr <- deserialiseOrFail @SequentialRef (LBS.fromStrict (view refLogUpdData tx))
& orThrow UnsupportedFormat
case sr of
SequentialRef n (AnnotatedHashRef (Just rhh) blkh) -> pure (n,rhh,blkh)
_ -> throwIO UnsupportedFormat
readTx :: (MonadIO m, MonadError OperationError m)
=> AnyStorage
-> HashRef
-> m (Integer, HashRef, RepoHead, HashRef)
readTx sto href = do
tx <- getBlock sto (fromHashRef href)
>>= orThrowError MissedBlockError
<&> deserialiseOrFail @(RefLogUpdate L4Proto)
>>= orThrowError UnsupportedFormat
(n,rhh,blkh) <- unpackTx tx
rh <- runExceptT (readFromMerkle sto (SimpleKey (fromHashRef rhh)))
>>= orThrowError IncompleteData
<&> deserialiseOrFail @RepoHead
>>= orThrowError UnsupportedFormat
missed <- S.head_ (findMissedBlocks2 sto blkh) <&> isJust
when missed do
throwError IncompleteData
pure (n, rhh, rh, blkh)
readRepoHeadFromTx :: MonadIO m
=> AnyStorage
-> HashRef
-> m (Maybe RepoHead)
readRepoHeadFromTx sto href = runMaybeT do
tx <- getBlock sto (fromHashRef href) >>= toMPlus
<&> deserialiseOrFail @(RefLogUpdate L4Proto)
>>= toMPlus
(n,rhh,_) <- unpackTx tx
runExceptT (readFromMerkle sto (SimpleKey (fromHashRef rhh)))
>>= toMPlus
<&> deserialiseOrFail @RepoHead
>>= toMPlus
data BundleMeta =
BundleMeta
{ bundleHash :: HashRef
, bundleEncrypted :: Bool
}
deriving stock (Show,Generic)
data BundleWithMeta =
BundleWithMeta
{ bundleMeta :: BundleMeta
, bundlebBytes :: LBS
}
deriving stock (Generic)
readBundle :: (MonadIO m, MonadError OperationError m, GroupKeyOperations m)
=> AnyStorage
-> RepoHead
-> HashRef
-> m BundleWithMeta
readBundle sto rh ref = do
obj <- getBlock sto (fromHashRef ref)
>>= orThrow MissedBlockError
let q = tryDetect (fromHashRef ref) obj
case q of
Merkle t -> do
let meta = BundleMeta ref False
BundleWithMeta meta <$>
readFromMerkle sto (SimpleKey key)
MerkleAnn (MTreeAnn {_mtaCrypt = EncryptGroupNaClSymm gkh _}) -> do
ke <- loadKeyrings (HashRef gkh)
let meta = BundleMeta ref True
BundleWithMeta meta <$> readFromMerkle sto (ToDecryptBS ke key)
_ -> throwError UnsupportedFormat
where
key = fromHashRef ref
readBundleRefs :: (MonadIO m)
=> AnyStorage
-> HashRef
-> m (Either [HashRef] [HashRef])
readBundleRefs sto bunh = do
r <- S.toList_ $
walkMerkle @[HashRef] (fromHashRef bunh) (getBlock sto) $ \case
Left h -> S.yield (Left h)
Right ( bundles :: [HashRef] ) -> do
mapM_ (S.yield . Right) bundles
let missed = lefts r
if not (null missed) then do
pure (Left (fmap HashRef missed))
else do
pure (Right $ rights r)
type GitPack = LBS.ByteString
type UnpackedBundle = (Word32, Word32, [GitHash], GitPack)
unpackPackMay :: LBS.ByteString -> Maybe UnpackedBundle
unpackPackMay co = result $ flip runGetOrFail co do
w <- getWord32be
v <- getWord32be
idx <- lookAheadE (getLazyByteString (fromIntegral w) <&> deserialiseOrFail @[GitHash])
>>= either (fail.show) pure
pack <- getRemainingLazyByteString
pure (w,v,idx,pack)
where
result = \case
Left{} -> Nothing
Right (_,_,r) -> Just r
data WriteBundleEnv =
WriteBundleEnvPlain
{ wbeHead :: RepoHead
, wbeStorage :: AnyStorage
}
| WriteBundleEnvEnc
{ wbeSk1 :: SipKey
, wbeSk2 :: SipKey
, wbeHead :: RepoHead
, wbeGk0 :: GK0
, wbeGks :: GroupSecret
, wbeStorage :: AnyStorage
}
newWriteBundleEnv :: (MonadIO m, GroupKeyOperations m) => AnyStorage -> RepoHead -> m WriteBundleEnv
newWriteBundleEnv sto rh = case _repoHeadGK0 rh of
Nothing -> do
pure $ WriteBundleEnvPlain rh sto
Just gk0h -> do
gk0 <- runExceptT (readGK0 sto gk0h)
>>= either throwIO pure
gks <- openGroupKey gk0
>>= orThrow (GroupKeyNotFound 3)
pure $ WriteBundleEnvEnc
{ wbeSk1 = SipKey 2716370006254639645 507093936407764973
, wbeSk2 = SipKey 9209704780415729085 272090086441077315
, wbeHead = rh
, wbeGk0 = gk0
, wbeGks = gks
, wbeStorage = sto
}
makeNonceForBundle :: Monad m => WriteBundleEnv -> LBS.ByteString -> m ByteString
makeNonceForBundle env lbs = do
let piece = ( LBS.take (fromIntegral defBlockSize * 2) lbs
<> serialise (wbeHead env)
) & hashObject @HbSync & serialise & LBS.drop 1 & LBS.toStrict
pure piece
writeBundle :: MonadIO m => WriteBundleEnv -> LBS.ByteString -> m HashRef
writeBundle env lbs = do
case env of
WriteBundleEnvPlain{..} -> do
writeAsMerkle wbeStorage lbs <&> HashRef
WriteBundleEnvEnc{..} -> do
let bsStream = readChunkedBS lbs defBlockSize
nonce <- makeNonceForBundle env lbs
let (SipHash a) = BA.sipHash wbeSk1 nonce
let (SipHash b) = BA.sipHash wbeSk2 nonce
let source = ToEncryptSymmBS wbeGks
(Right wbeGk0)
nonce
bsStream
NoMetaData
(Just (EncryptGroupNaClSymmBlockSIP (a,b)))
th <- runExceptT (writeAsMerkle wbeStorage source)
>>= orThrow StorageError
pure $ HashRef th

View File

@ -0,0 +1,68 @@
module HBS2.Git.Local where
import HBS2.Prelude.Plated
import Data.ByteString.Base16 qualified as B16
import Text.InterpolatedString.Perl6 (qc)
import Data.ByteString.Char8 qualified as BS
import Data.ByteString.Char8 (ByteString)
import Codec.Serialise
data SHA1 = SHA1
deriving stock(Eq,Ord,Data,Generic)
newtype GitHash = GitHash ByteString
deriving stock (Eq,Ord,Data,Generic,Show)
deriving newtype Hashable
instance Serialise GitHash
instance IsString GitHash where
fromString s = GitHash (B16.decodeLenient (BS.pack s))
instance FromStringMaybe GitHash where
fromStringMay s = either (const Nothing) (pure . GitHash) (B16.decode bs)
where
bs = BS.pack s
instance Pretty GitHash where
pretty (GitHash s) = pretty @String [qc|{B16.encode s}|]
newtype GitRef = GitRef { unGitRef :: ByteString }
deriving stock (Eq,Ord,Data,Generic,Show)
deriving newtype (IsString,Monoid,Semigroup,Hashable)
instance Serialise GitRef
mkGitRef :: ByteString -> GitRef
mkGitRef = GitRef
instance Pretty GitRef where
pretty (GitRef x) = pretty @String [qc|{x}|]
data GitObjectType = Commit | Tree | Blob
deriving stock (Eq,Ord,Show,Generic)
instance Serialise GitObjectType
instance IsString GitObjectType where
fromString = \case
"commit" -> Commit
"tree" -> Tree
"blob" -> Blob
x -> error [qc|invalid git object type {x}|]
instance FromStringMaybe GitObjectType where
fromStringMay = \case
"commit" -> Just Commit
"tree" -> Just Tree
"blob" -> Just Blob
_ -> Nothing
instance Pretty GitObjectType where
pretty = \case
Commit -> pretty @String "commit"
Tree -> pretty @String "tree"
Blob -> pretty @String "blob"

View File

@ -0,0 +1,66 @@
module HBS2.Git.Local.CLI where
import HBS2.Prelude
import System.FilePath
import HBS2.System.Dir
import System.Environment hiding (setEnv)
import Control.Monad.Trans.Maybe
import Control.Applicative
import System.Process.Typed
import Data.ByteString.Lazy (ByteString)
import Data.ByteString.Lazy.Char8 qualified as LBS8
import Text.InterpolatedString.Perl6 (qc)
findGitDir :: MonadIO m => m (Maybe FilePath)
findGitDir = findGitDir' =<< pwd
where
findGitDir' dir = do
let gd = dir </> ".git"
exists <- liftIO $ doesDirectoryExist gd
if exists
then return $ Just gd
else let parentDir = takeDirectory dir
in if parentDir == dir -- we've reached the root directory
then return Nothing
else findGitDir' parentDir
checkIsBare :: MonadIO m => Maybe FilePath -> m Bool
checkIsBare fp = do
let wd = maybe id setWorkingDir fp
(code,s,_) <- readProcess ( shell [qc|git config --local core.bare|]
& setStderr closed & wd
)
case (code, LBS8.words s) of
(ExitSuccess, "true" : _) -> pure True
_ -> pure False
gitDir :: MonadIO m => m (Maybe FilePath)
gitDir = runMaybeT do
byEnv <- liftIO $ lookupEnv "GIT_DIR"
byDir <- findGitDir
byBare <- checkIsBare Nothing >>= \case
True -> pwd >>= expandPath <&> Just
False -> pure Nothing
toMPlus (byEnv <|> byDir <|> byBare)
gitRunCommand :: MonadIO m
=> String
-> m (Either ExitCode ByteString)
gitRunCommand cmd = do
let procCfg = setStdin closed $ setStderr closed $ shell cmd
(code, out, _) <- readProcess procCfg
case code of
ExitSuccess -> pure (Right out)
e -> pure (Left e)

155
hbs21-git/hbs21-git.cabal Normal file
View File

@ -0,0 +1,155 @@
cabal-version: 3.0
name: hbs21-git
version: 0.1.0.0
-- synopsis:
-- description:
license: BSD-3-Clause
license-file: LICENSE
author: Dmitry Zuikov
maintainer: dzuikov@gmail.com
-- copyright:
category: System
build-type: Simple
-- extra-doc-files: CHANGELOG.md
-- extra-source-files:
common shared-properties
ghc-options:
-Wall
-fno-warn-type-defaults
-threaded
-rtsopts
-O2
"-with-rtsopts=-N4 -A64m -AL256m -I0"
default-language: GHC2021
default-extensions:
ApplicativeDo
, BangPatterns
, BlockArguments
, ConstraintKinds
, DataKinds
, DeriveDataTypeable
, DeriveGeneric
, DerivingStrategies
, DerivingVia
, ExtendedDefaultRules
, FlexibleContexts
, FlexibleInstances
, GADTs
, GeneralizedNewtypeDeriving
, ImportQualifiedPost
, LambdaCase
, MultiParamTypeClasses
, OverloadedStrings
, QuasiQuotes
, RecordWildCards
, ScopedTypeVariables
, StandaloneDeriving
, TupleSections
, TypeApplications
, TypeFamilies
build-depends:
hbs2-core
, hbs2-peer
, hbs2-storage-simple
, hbs2-keyman
, db-pipe
, suckless-conf
, attoparsec
, atomic-write
, bytestring
, binary
, containers
, directory
, exceptions
, filepath
, filepattern
, interpolatedstring-perl6
, memory
, microlens-platform
, mtl
, safe
, serialise
, streaming
, stm
, text
, time
, timeit
, transformers
, typed-process
, unordered-containers
, unliftio
, unliftio-core
, zlib
, prettyprinter
, prettyprinter-ansi-terminal
, random
, vector
, unix
library hbs2-git-client-lib
import: shared-properties
exposed-modules:
HBS2.Git.Local
HBS2.Git.Local.CLI
HBS2.Git.Data.Tx
HBS2.Git.Data.GK
HBS2.Git.Data.RefLog
HBS2.Git.Client.Prelude
HBS2.Git.Client.App.Types
HBS2.Git.Client.App.Types.GitEnv
HBS2.Git.Client.App
HBS2.Git.Client.Config
HBS2.Git.Client.State
HBS2.Git.Client.RefLog
HBS2.Git.Client.Export
HBS2.Git.Client.Import
HBS2.Git.Client.Progress
build-depends: base
, base16-bytestring
, binary
, unix
hs-source-dirs: hbs2-git-client-lib
executable git-hbs21
import: shared-properties
main-is: Main.hs
-- other-modules:
-- other-extensions:
build-depends:
base, hbs2-git-client-lib
, binary
, vector
, optparse-applicative
hs-source-dirs: git-hbs21
default-language: GHC2021
executable git-remote-hbs21
import: shared-properties
main-is: Main.hs
-- other-modules:
-- other-extensions:
build-depends:
base, hbs2-git-client-lib
, binary
, vector
, optparse-applicative
hs-source-dirs: git-remote-hbs21
default-language: GHC2021