module Darcs.UI.PatchHeader ( getLog , getAuthor , updatePatchHeader, AskAboutDeps(..) , HijackT, HijackOptions(..) , runHijackT ) where import Prelude () import Darcs.Prelude import Darcs.Patch ( IsRepoType, RepoPatch, PrimPatch, PrimOf, fromPrims , effect , summaryFL ) import Darcs.Patch.Apply ( ApplyState ) import Darcs.Patch.Info ( PatchInfo, piAuthor, piName, piLog, piDateString, patchinfo, isInverted, invertName, ) import Darcs.Patch.Named.Wrapped ( infopatch, getdeps, adddeps ) import Darcs.Patch.PatchInfoAnd ( PatchInfoAnd, n2pia, hopefully, info ) import Darcs.Patch.Prim ( canonizeFL ) import Darcs.Patch.Witnesses.Ordered ( FL(..), (+>+) ) import Darcs.Repository ( Repository ) import Darcs.Util.Lock ( readTextFile , writeTextFile ) import Darcs.UI.External ( editFile ) import Darcs.UI.Flags ( getEasyAuthor, promptAuthor, getDate ) import qualified Darcs.UI.Options.All as O import qualified Darcs.UI.SelectChanges as S ( PatchSelectionOptions(..) ) import Darcs.UI.SelectChanges ( askAboutDepends ) import qualified Darcs.Util.Diff as D ( DiffAlgorithm ) import Darcs.Util.English ( capitalize ) import Darcs.Util.Global ( darcsLastMessage ) import Darcs.Util.Path ( FilePathLike, toFilePath ) import Darcs.Util.Prompt ( PromptConfig(..), askUser, promptChar, promptYorn ) import Darcs.Util.Printer ( text, ($$), vcat, prefixLines, renderString ) import qualified Darcs.Util.Ratified as Ratified ( hGetContents ) import Darcs.Util.Tree ( Tree ) import Control.Exception ( catch, IOException ) import Control.Monad ( when, void ) import Control.Monad.Trans ( liftIO ) import Control.Monad.Trans.State.Strict ( StateT(..), evalStateT, get, put ) import Data.List ( isPrefixOf ) import System.Exit ( exitSuccess ) import System.IO ( stdin ) data PName = FlagPatchName String | PriorPatchName String | NoPatchName -- | Options for how to deal with the situation where we are somehow -- modifying a patch that is not our own data HijackOptions = IgnoreHijack -- ^ accept all hijack requests | RequestHijackPermission -- ^ prompt once, accepting subsequent hijacks if yes | AlwaysRequestHijackPermission -- ^ always prompt -- | Transformer for interactions with a hijack warning state that we -- need to thread through type HijackT = StateT HijackOptions -- | Get the patch name and long description from one of -- -- * the configuration (flags, defaults, hard-coded) -- -- * an existing log file -- -- * stdin (e.g. a pipe) -- -- * a text editor -- -- It ensures the patch name is not empty nor starts with the prefix TAG. -- -- The last result component is a possible path to a temporary file that should be removed later. getLog :: forall prim wX wY . PrimPatch prim => Maybe String -- ^ patchname option -> Bool -- ^ pipe option -> O.Logfile -- ^ logfile option -> Maybe O.AskLongComment -- ^ askLongComment option -> Maybe (String, [String]) -- ^ possibly an existing patch name and long description -> FL prim wX wY -- ^ changes to record -> IO (String, [String], Maybe String) -- ^ patch name, long description and possibly the path -- to the temporary file that should be removed later getLog m_name has_pipe log_file ask_long m_old chs = go has_pipe log_file ask_long where go True _ _ = do p <- case patchname_specified of FlagPatchName p -> return p PriorPatchName p -> return p NoPatchName -> prompt_patchname False putStrLn "What is the log?" thelog <- lines `fmap` Ratified.hGetContents stdin return (p, thelog, Nothing) go _ (O.Logfile { O._logfile = Just f }) _ = do mlp <- readTextFile f `catch` (\(_ :: IOException) -> return []) firstname <- case (patchname_specified, mlp) of (FlagPatchName p, []) -> return p (_, p:_) -> if badName p then prompt_patchname True else return p -- logfile trumps prior! (PriorPatchName p, []) -> return p (NoPatchName, []) -> prompt_patchname True append_info f firstname when (ask_long == Just O.YesEditLongComment) (void $ editFile f) (name, thelog) <- read_long_comment f firstname return (name, thelog, if O._rmlogfile log_file then Just $ toFilePath f else Nothing) go _ _ (Just O.YesEditLongComment) = case patchname_specified of FlagPatchName p -> actually_get_log p PriorPatchName p -> actually_get_log p NoPatchName -> actually_get_log "" go _ _ (Just O.NoEditLongComment) = case patchname_specified of FlagPatchName p -> return (p, default_log, Nothing) -- record (or amend) -m PriorPatchName p -> return (p, default_log, Nothing) -- amend NoPatchName -> do p <- prompt_patchname True -- record return (p, [], Nothing) go _ _ (Just O.PromptLongComment) = case patchname_specified of FlagPatchName p -> prompt_long_comment p -- record (or amend) -m PriorPatchName p -> prompt_long_comment p NoPatchName -> prompt_patchname True >>= prompt_long_comment go _ _ Nothing = case patchname_specified of FlagPatchName p -> return (p, default_log, Nothing) -- record (or amend) -m PriorPatchName "" -> actually_get_log "" PriorPatchName p -> return (p, default_log, Nothing) NoPatchName -> actually_get_log "" patchname_specified = case (m_name, m_old) of (Just name, _) | badName name -> NoPatchName | otherwise -> FlagPatchName name (Nothing, Just (name,_)) -> PriorPatchName name (Nothing, Nothing) -> NoPatchName badName "" = True badName n = "TAG" `isPrefixOf` n default_log = case m_old of Nothing -> [] Just (_,l) -> l prompt_patchname retry = do n <- askUser "What is the patch name? " if badName n then if retry then prompt_patchname retry else fail "Bad patch name!" else return n prompt_long_comment oldname = do y <- promptYorn "Do you want to add a long comment?" if y then actually_get_log oldname else return (oldname, [], Nothing) actually_get_log p = do let logf = darcsLastMessage -- TODO: make sure encoding used for logf is the same everywhere -- probably should be locale because the editor will assume it writeTextFile logf $ unlines $ p : default_log append_info logf p _ <- editFile logf (name,long) <- read_long_comment logf p if badName name then do putStrLn "WARNING: empty or incorrect patch name!" pn <- prompt_patchname True return (pn, long, Nothing) else return (name,long,Just logf) read_long_comment :: FilePathLike p => p -> String -> IO (String, [String]) read_long_comment f oldname = do t <- readTextFile f let filter_out_info = filter (not.("#" `isPrefixOf`)) case reverse $ dropWhile null $ reverse $ filter_out_info t of [] -> return (oldname, []) (n:ls) -> return (n, ls) append_info f oldname = do fc <- readTextFile f writeTextFile f $ renderString $ vcat (map text $ if null fc then [oldname] else fc) $$ text "# Please enter the patch name in the first line, and" $$ text "# optionally, a long description in the following lines." $$ text "#" $$ text "# Lines starting with '#' will be ignored." $$ text "#" $$ text "#" $$ text "# Summary of selected changes:" $$ text "#" $$ prefixLines (text "#") (summaryFL chs) -- |specify whether to ask about dependencies with respect to a particular repository, or not data AskAboutDeps rt p wR wU wT = AskAboutDeps (Repository rt p wR wU wT) | NoAskAboutDeps -- | Run a job that involves a hijack confirmation prompt. -- -- See 'RequestHijackPermission' for initial values runHijackT :: Monad m => HijackOptions -> HijackT m a -> m a runHijackT = flip evalStateT -- | Update the metadata for a patch. -- This potentially involves a bit of interactivity, so we may return @Nothing@ -- if there is cause to abort what we're doing along the way updatePatchHeader :: forall rt p wX wY wR wU wT . (IsRepoType rt, RepoPatch p, ApplyState p ~ Tree) => String -- ^ verb: command name -> AskAboutDeps rt p wR wU wT -> S.PatchSelectionOptions -> D.DiffAlgorithm -> Bool -- keepDate -> Bool -- selectAuthor -> Maybe String -- author -> Maybe String -- patchname -> Maybe O.AskLongComment -> PatchInfoAnd rt p wT wX -> FL (PrimOf p) wX wY -> HijackT IO (Maybe String, PatchInfoAnd rt p wT wY) updatePatchHeader verb ask_deps pSelOpts da nKeepDate nSelectAuthor nAuthor nPatchname nAskLongComment oldp chs = do let newchs = canonizeFL da (effect oldp +>+ chs) let old_pdeps = getdeps $ hopefully oldp newdeps <- case ask_deps of AskAboutDeps repository -> liftIO $ askAboutDepends repository newchs pSelOpts old_pdeps NoAskAboutDeps -> return old_pdeps let old_pinf = info oldp prior = (piName old_pinf, piLog old_pinf) date <- if nKeepDate then return (piDateString old_pinf) else liftIO $ getDate False new_author <- getAuthor verb nSelectAuthor nAuthor old_pinf liftIO $ do (new_name, new_log, mlogf) <- getLog nPatchname False (O.Logfile Nothing False) nAskLongComment (Just prior) chs let maybe_invert = if isInverted old_pinf then invertName else id new_pinf <- maybe_invert `fmap` patchinfo date new_name new_author new_log let newp = n2pia (adddeps (infopatch new_pinf (fromPrims newchs)) newdeps) return (mlogf, newp) -- | @getAuthor@ tries to return the updated author for the patch. -- There are two different scenarios: -- -- * [explicit] Either we want to override the patch author, be it by -- prompting the user (@select@) or having them pass it in from -- the UI (@new_author@), or -- -- * [implicit] We want to keep the original author, in which case we -- also double-check that we are not inadvertently \"hijacking\" -- somebody else's patch (if the patch author is not the same as the -- repository author, we give them a chance to abort the whole -- operation) getAuthor :: String -- ^ verb: command name -> Bool -- ^ select: prompt for new auhor -> Maybe String -- ^ new author: explict new author -> PatchInfo -- ^ patch to update -> HijackT IO String getAuthor _ True _ _ = do auth <- liftIO $ promptAuthor False True return auth getAuthor _ False (Just new) _ = return new getAuthor verb False Nothing pinfo = do whitelist <- liftIO $ getEasyAuthor hj <- get if orig `elem` whitelist || canIgnore hj then allowHijack else do hijackResp <- liftIO $ askAboutHijack hj case hijackResp of 'y' -> allowHijack 'a' -> put IgnoreHijack >> allowHijack _ -> liftIO exitSuccess where askAboutHijack hj = promptChar (PromptConfig msg opts [] Nothing []) where msg = "You're not " ++ orig ++"! " ++ capitalize verb ++ " anyway? " opts = case hj of AlwaysRequestHijackPermission -> "yn" _ -> "yna" canIgnore IgnoreHijack = True canIgnore RequestHijackPermission = False canIgnore AlwaysRequestHijackPermission = False allowHijack = return orig orig = piAuthor pinfo