{-# LANGUAGE DeriveDataTypeable, OverloadedStrings, ScopedTypeVariables #-} import Control.Applicative ( (<$>) ) import Control.Monad ( forM, when, forM_ ) import Control.Monad.Trans ( lift ) import Control.Monad.State ( modify ) import Control.Monad.IO.Class( MonadIO(..) ) import Data.IORef.MonadIO import System.Console.CmdLib hiding ( summary ) import System.FSLib.IO import System.FSLib.Hash ( Hash(..), sha256, encodeBase16, encodeBase64u ) import System.FSLib.StoreMonad import System.Environment ( getEnvironment ) import Text.PrettyPrint.Leijen.Text ( displayT, renderPretty ) import qualified Data.Text as T import Data.Patch( invert ) import Data.Patch.Apply ( applyToState ) import Data.Patch.Show ( Display, displayEC, summariseEC ) import Data.Patch.ObjectMap ( UUID(..), Object(..), findPath, ObjectType(..), DirContent ) import Data.Patch.Commute ( commute, commuteRLFL ) import Data.Patch.Invert ( invertFL ) import Data.Patch.Prim.V3 () import Data.Patch.Prim.V3.Core ( Prim ) import Data.Patch.Info ( PatchInfo, makePI, formatPI, getNiceDate ) import Data.Patch.Diff ( Diff(..), Branched(..), leftmost ) import Data.Path ( (), parsePath, Absolute, Relative, SubPath, parent, Split(..) , pathToString, eitherPath, ioAbsolute ) import Witnesses.Sealed import Witnesses.Ordered ( (:>)(..), FL ) import qualified Data.Text.Lazy as TL import qualified Data.Path as P ( file ) import qualified Data.ByteString as B import qualified Data.ByteString.Char8 as BC import qualified Data.Map as M import qualified Data.Set as S import qualified Codec.Binary.Base16 as B16 import qualified Codec.Binary.Base64Url as B64 import System.PosixCompat.User ( userGecos, getRealUserID, getUserEntryForID ) import Types import UUID import Repository import Shuffle data ObjectSpec = ObjectId UUID | ObjectPath SubPath | NoObject deriving (Eq, Typeable, Data) data Cmd = Init { force :: Bool } | Add { uuid :: Maybe UUID, file :: String, update :: Bool, force :: Bool } | Remove { file :: String } | Mv { file :: String, target :: String } | Record { message :: String, author :: String } | Unrecord {} | Cat { object :: ObjectSpec, inPristine :: Bool } | Ls { object :: ObjectSpec, inPristine :: Bool } | Changes { details :: Bool, summary :: Bool } | Whatsnew { summary :: Bool } | Status { summary :: Bool } | Pull { remote :: Either Absolute Relative } deriving (Eq, Typeable, Data) instance Attributes Cmd where attributes _ = group "Options" [ uuid %> [ Help "Provide a fixed UUID.", Default (Nothing :: Maybe UUID) ], message %> [ Short "m", Help "The patch name to record.", Default ("unnamed patch" :: String) ], summary %> [ Short "s", Help "Summarise patch(es)." ], remote %> [ Required True, Positional 0 ], object %> [ Required True, Positional 0 ], file %> [ Required True, Positional 0 ], target %> [ Required True, Positional 1 ] ] readFlag _ = readCommon <+< uuid <+< object <+< eitherPath where uuid :: String -> Maybe UUID uuid id | null id = Nothing | length id == 64, Just dec <- B16.decode id = Just $ UUID $ B.pack dec | length id == 44, Just dec <- B64.decode id = Just $ UUID $ B.pack dec | last id /= '=', Just id' <- uuid (id ++ "=") = Just id' | otherwise = Nothing object id = case uuid id of Just x -> ObjectId x Nothing -> case parsePath id of Just x -> ObjectPath x Nothing -> NoObject instance RecordCommand Cmd where mode_summary Init {} = "Create a new repository." mode_summary Ls {} = "List repository contents." mode_summary Add {} = "Add an object to shadow." mode_summary Remove {} = "Remove an object from shadow." mode_summary Cat {} = "Display current version of an object." mode_summary Whatsnew {} = "Display the difference between pristine and shadow." mode_summary Status {} = "Summarise the difference between pristine and shadow." mode_summary Changes {} = "List patches recorded in the repository." mode_summary Record {} = "Record the difference between pristine and shadow as a patch." mode_summary Unrecord {} = "Remove the topmost recorded patch." mode_summary Mv {} = "Rename a file in working and in shadow." mode_summary Pull {} = "Pull patches from another repository." rec_options Cat {} = uuid %> [ Positional 0, Required True ] rec_options Ls {} = object %> [ Positional 0, Required False, Default (ObjectId rootUUID) ] rec_options Status {} = summary %> [ Default True ] rec_options _ = noAttributes renderString = TL.unpack . displayT . renderPretty 1.0 80 toShadow uuid path = do isdir <- doesDirectoryExist path editShadow uuid $ if isdir then Directory M.empty else Blob (readFileBS path) TText NoHash withPaths :: [String] -> ([SubPath] -> Gorsvet a) -> Gorsvet a withPaths paths act = do paths' <- mapM checkPath paths act paths' where checkPath p = case parsePath p of Nothing -> fail $ "Invalid path: " ++ p Just p' -> return p' getInfo :: Cmd -> Gorsvet PatchInfo getInfo x = do date <- getNiceDate gecos <- takeWhile (/=',') . userGecos <$> (liftIO . getUserEntryForID =<< liftIO getRealUserID) envmail <- lookup "EMAIL" <$> liftIO getEnvironment let aname = case (author x, gecos, envmail) of ("", "", Nothing) -> "anonymous" ("", "", Just em) -> "<" ++ em ++ ">" ("", _, Just em) -> gecos ++ " <" ++ em ++ ">" (_, _, _) -> author x name <- return $ T.pack $ message x return $ makePI [ ("Author", T.pack aname) , ("Date", date) , ("Name", name) ] cmd_mv :: Cmd -> Gorsvet () cmd_mv x = withPaths [file x, target x] $ \[from, to] -> do Just id <- findUUID from demanifest from id manifest to id rename from to cmd_add :: Cmd -> Gorsvet () cmd_add x = withPaths [file x] $ \[path] -> do id <- case (uuid x, update x) of (Just id, True) -> return id (Just id, False) -> case force x of True -> return id False -> fail "Object already exists. Use --force." (Nothing, True) -> findUUID path >>= \(Just x) -> return x (Nothing, False) -> do mb <- findPath path rootUUID =<< shadow case (mb, force x) of (Just id, False) -> fail "Object already exists. Use --update or --force." _ -> makeUUID manifest path id toShadow id path return () cmd_browse :: Cmd -> Gorsvet () cmd_browse x = case object x of NoObject -> fail "An object specifier is required" ObjectId id -> dump id ObjectPath path -> findUUID path >>= \(Just x) -> dump x where dump id = do get <- omGet <$> (if inPristine x then pristine else shadow) obj <- get id case obj of Just obj -> (liftIO . putStr) =<< formatObj obj Nothing -> fail "Object not found" cmd_whatsnew :: Cmd -> Gorsvet () cmd_whatsnew x = do Sealed branched <- unrecordedChanges doc <- display (leftmost branched) =<< shadow liftIO . putStrLn $ renderString doc where display | summary x = summariseEC | otherwise = displayEC unrecordedChanges :: Gorsvet (Sealed (Branched Prim a)) unrecordedChanges = do pris <- pristine sha <- shadow unFreeLeft <$> diff pris sha main = getArgs >>= dispatchR [] >>= \x -> case x of Init {} -> enhancedIO $ do when (force x) $ removeRecursively gsdir initRepository Add {} -> gorsvetRW $ cmd_add x Ls {} -> gorsvetRO $ cmd_browse x Cat {} -> gorsvetRO $ cmd_browse x Remove {} -> gorsvetRW $ withPaths [file x] $ \[path] -> do Just id <- findUUID path demanifest path id Mv {} -> gorsvetRW $ withPaths [file x, target x] $ \[from, to] -> do Just id <- findUUID from demanifest from id manifest to id rename from to Whatsnew {} -> gorsvetRO $ cmd_whatsnew x Status {} -> gorsvetRO $ cmd_whatsnew x Changes {} -> gorsvetRO $ do ord <- order context <- newIORef =<< pristine forM_ ord $ \ih -> do Just ph <- M.lookup ih <$> inventory inf <- load ih let hashn = encodeBase16 ph fn = objdir B.take 2 hashn B.drop 2 hashn liftIO . putStr . T.unpack . formatPI $ inf liftIO . putStrLn $ "Info-Hash: " ++ BC.unpack (encodeBase64u ih) liftIO . putStrLn $ "Stored-In: " ++ pathToString fn when (details x || summary x) $ do patch :: FL Prim x y <- load ph let offset = unlines . map (' ':) . lines display fun = liftIO . putStr . offset . renderString =<< fun when (summary x) $ do liftIO . putStrLn $ "Change-Summary: " display $ summariseEC patch =<< readIORef context when (details x) $ do liftIO . putStrLn $ "Patch: " display $ displayEC patch =<< readIORef context context' <- applyToState (invert patch) =<< readIORef context writeIORef context context' liftIO . putStrLn $ "" Record {} -> gorsvetRW $ do Sealed branched <- unrecordedChanges info <- getInfo x hash <- store $ leftmost branched ihash <- store $ info editInventory (M.insert ihash hash) editOrder (ihash:) pristine' <- applyToState (leftmost branched) =<< pristine editRepository $ \x -> x { rPristine = pristine' } return () Unrecord {} -> gorsvetRW $ do ihash <- head <$> order Just phash <- M.lookup ihash <$> inventory patch :: FL Prim x y <- load phash editInventory (M.delete ihash) editOrder tail applyToState (invert patch) =<< pristine return () Pull {} -> gorsvetRW $ do rpath <- case remote x of Left y -> return y Right y -> liftIO $ ioAbsolute y modify $ \x -> x { gsRemotes = rpath : gsRemotes x } rrepo <- readRepository rpath rrepo' <- coShuffle rrepo -- our repository and rrepo' have a longest possible common prefix now ours <- inventory let extra = M.keysSet $ M.difference ours (rInventory rrepo') missingM = M.difference (rInventory rrepo') ours missing = M.keysSet missingM missing' = takeWhile (`S.member` missing) $ rOrder rrepo' extra' <- takeWhile (`S.member` extra) <$> order forM (S.toList missing) $ \ih -> do store =<< (load ih :: Gorsvet PatchInfo) -- add the remote patches to our inventory editRepository $ \x -> x { rInventory = M.union (rInventory x) missingM } when (S.size extra /= length extra') $ fail "BUG: coShuffle problem with extra" when (S.size missing /= length missing') $ fail "BUG: coShuffle problem with missing" liftIO . putStrLn $ "\nPulling patches:" forM missing' $ \ih -> do inf :: PatchInfo <- load ih liftIO . putStr . T.unpack . formatPI $ inf r <- repo extraFL <- loadFL r $ reverse extra' missingFL <- loadFL r $ reverse missing' (missingFL' :> _) <- commuteRLFL $ invertFL extraFL :> missingFL editRepositoryM $ \r -> storeFL r missingFL' editRepositoryM $ \r -> do pristine' <- applyToState missingFL' =<< pristine shadow' <- applyToState missingFL' =<< shadow return $ r { rPristine = pristine', rShadow = shadow' } editRepository $ \ x -> x { rOrder = missing' ++ rOrder x }