nix-diff-1.0.20: src/Nix/Diff/Render/HumanReadable.hs
{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Nix.Diff.Render.HumanReadable where
import Control.Monad (forM_)
import Control.Monad.Reader (MonadReader, ReaderT (runReaderT), ask, local)
import Control.Monad.Writer(MonadWriter, Writer, tell, runWriter)
import Data.Set (Set)
import Data.Text (Text)
import Numeric.Natural (Natural)
import qualified Control.Monad.Reader
import qualified Data.Map
import qualified Data.Set
import qualified Data.Text as Text
import qualified Patience
#if !MIN_VERSION_base(4,15,1)
import Control.Monad.Fail (MonadFail)
#endif
import Nix.Diff
import Nix.Diff.Types
data RenderContext = RenderContext
{ orientation :: Orientation
, tty :: TTY
, indent :: Natural
, context :: Maybe Natural
}
newtype Render a = Render { unRender :: ReaderT RenderContext (Writer Text) a}
deriving
( Functor
, Applicative
, Monad
, MonadReader RenderContext
, MonadWriter Text
)
runRender :: Render a -> RenderContext -> (a, Text)
runRender render rc = runWriter $ runReaderT (unRender render) rc
runRender' :: Render () -> RenderContext -> Text
runRender' render = snd . runRender render
echo :: Text -> Render ()
echo text = do
RenderContext { indent } <- ask
let n = fromIntegral indent
tellLn (Text.replicate n " " <> text)
where
tellLn line = tell (line <> "\n")
indented :: Natural -> Render a -> Render a
indented n = local adapt
where
adapt context = context { indent = indent context + n }
data TTY = IsTTY | NotTTY
-- | Color text red
red :: TTY -> Text -> Text
red IsTTY text = "\ESC[1;31m" <> text <> "\ESC[0m"
red NotTTY text = text
-- | Color text background red
redBackground :: Orientation -> TTY -> Text -> Text
redBackground Line IsTTY text = "\ESC[41m" <> prefix <> "\ESC[0m" <> suffix
where
(prefix, suffix) = Text.break lineBoundary text
redBackground Word IsTTY text = "\ESC[41m" <> prefix <> "\ESC[0m" <> suffix
where
(prefix, suffix) = Text.break wordBoundary text
redBackground Character IsTTY text = "\ESC[41m" <> text <> "\ESC[0m"
redBackground Line NotTTY text = "- " <> text
redBackground _ NotTTY text = "←" <> text <> "←"
-- | Color text green
green :: TTY -> Text -> Text
green IsTTY text = "\ESC[1;32m" <> text <> "\ESC[0m"
green NotTTY text = text
-- | Color text background green
greenBackground :: Orientation -> TTY -> Text -> Text
greenBackground Line IsTTY text = "\ESC[42m" <> prefix <> "\ESC[0m" <> suffix
where
(prefix, suffix) = Text.break lineBoundary text
greenBackground Word IsTTY text = "\ESC[42m" <> prefix <> "\ESC[0m" <> suffix
where
(prefix, suffix) = Text.break wordBoundary text
greenBackground Character IsTTY text = "\ESC[42m" <> text <> "\ESC[0m"
greenBackground Line NotTTY text = "+ " <> text
greenBackground _ NotTTY text = "→" <> text <> "→"
-- | Color text grey
grey :: Orientation -> TTY -> Text -> Text
grey _ IsTTY text = "\ESC[1;2m" <> text <> "\ESC[0m"
grey Line NotTTY text = " " <> text
grey _ NotTTY text = text
-- | Format the left half of a diff
minus :: TTY -> Text -> Text
minus tty text = red tty ("- " <> text)
-- | Format the right half of a diff
plus :: TTY -> Text -> Text
plus tty text = green tty ("+ " <> text)
-- | Format text explaining a diff
explain :: Text -> Text
explain text = "• " <> text
{-| Utility to automate a common pattern of printing the two halves of a diff.
This passes the correct formatting function to each half
-}
renderWith :: Changed a -> ((Text -> Text, a) -> Render ()) -> Render ()
renderWith Changed{..} k = do
RenderContext { tty } <- ask
k (minus tty, before)
k (plus tty, now)
-- | Format the derivation outputs
renderOutputs :: Set Text -> Text
renderOutputs outputs =
":{" <> Text.intercalate "," (Data.Set.toList outputs) <> "}"
renderDiffHumanReadable :: DerivationDiff -> Render ()
renderDiffHumanReadable = \case
DerivationsAreTheSame -> pure ()
AlreadyCompared -> echo (explain "These two derivations have already been compared")
OnlyAlreadyComparedBelow {..} -> do
renderOutputStructure outputStructure
echo (explain "Skipping because only derivations that have already been compared and shown in the diff are below")
NamesDontMatch {..} -> do
renderOutputStructure outputStructure
echo (explain "The derivation names do not match")
OutputsDontMatch {..} -> do
renderOutputStructure outputStructure
echo (explain "The requested outputs do not match")
DerivationDiff {..} -> do
renderOutputStructure outputStructure
renderOutputsDiff outputsDiff
renderPlatformDiff platformDiff
renderBuilderDiff builderDiff
renderArgsDiff argumentsDiff
renderSrcDiff sourcesDiff
renderInputsDiff inputsDiff
renderEnvDiff envDiff
where
renderOutputStructure os =
renderWith os \(sign, (OutputStructure path outputs)) -> do
echo (sign (Text.pack path <> renderOutputs outputs))
renderOutputsDiff OutputsDiff{..} = do
ifExist extraOutputs \eo -> do
echo (explain "The set of outputs do not match:")
renderWith eo \(sign, extraOutputs') -> do
forM_ (Data.Map.toList extraOutputs') \(key, _value) -> do
echo (" " <> sign ("{" <> key <> "}"))
mapM_ renderOutputHashDiff outputHashDiff
renderOutputHashDiff OutputDiff{..} = do
echo (explain ("{" <> outputName <> "}:"))
echo (explain " Hash algorithm:")
renderWith hashDifference \(sign, hashAlgo) -> do
echo (" " <> sign hashAlgo)
renderPlatformDiff mpd =
ifExist mpd \pd -> do
echo (explain "The platforms do not match")
renderWith pd \(sign, platform) -> do
echo (" " <> sign platform)
renderBuilderDiff mbd =
ifExist mbd \bd -> do
echo (explain "The builders do not match")
renderWith bd \(sign, builder) -> do
echo (" " <> sign builder)
renderArgsDiff mad =
ifExist mad \(ArgumentsDiff ad) -> do
RenderContext { tty } <- ask
echo (explain "The arguments do not match")
let renderDiff (Patience.Old arg) =
echo (" " <> minus tty arg)
renderDiff (Patience.New arg) =
echo (" " <> plus tty arg)
renderDiff (Patience.Both arg _) =
echo (" " <> explain arg)
mapM_ renderDiff ad
renderSrcDiff SourcesDiff{..} = do
ifExist extraSrcNames \esn -> do
echo (explain "The set of input source names do not match:")
renderWith esn \(sign, names) -> do
forM_ names \name -> do
echo (" " <> sign name)
mapM_ renderSrcFileDiff srcFilesDiff
renderSrcFileDiff OneSourceFileDiff{..} = do
echo (explain ("The input source named `" <> srcName <> "` differs"))
ifExist srcContentDiff \scd -> do
text <- renderText scd
echo (" " <> text)
renderSrcFileDiff SomeSourceFileDiff{..} = do
echo (explain ("The input sources named `" <> srcName <> "` differ"))
renderWith srcFileDiff \(sign, paths) -> do
forM_ paths \path -> do
echo (" " <> sign (Text.pack path))
renderInputsDiff InputsDiff{..} = do
renderInputExtraNames inputExtraNames
mapM_ renderInputDerivationsDiff inputDerivationDiffs
renderInputExtraNames mien =
ifExist mien \ien -> do
echo (explain "The set of input derivation names do not match:")
renderWith ien \(sign, names) -> do
forM_ names \name -> do
echo (" " <> sign name)
renderInputDerivationsDiff OneDerivationDiff{..} = do
echo (explain ("The input derivation named `" <> drvName <> "` differs"))
indented 2 (renderDiffHumanReadable drvDiff)
renderInputDerivationsDiff SomeDerivationsDiff{..} = do
echo (explain ("The set of input derivations named `" <> drvName <> "` do not match"))
renderWith extraPartsDiff \(sign, extraPaths) -> do
forM_ (Data.Map.toList extraPaths) \(extraPath, outputs) -> do
echo (" " <> sign (Text.pack extraPath <> renderOutputs outputs))
renderEnvDiff Nothing =
echo (explain "Skipping environment comparison")
renderEnvDiff (Just EnvironmentsAreEqual) = pure ()
renderEnvDiff (Just EnvironmentDiff{..}) = do
echo (explain "The environments do not match:")
renderWith extraEnvDiff \(sign, extraEnv) -> do
forM_ (Data.Map.toList extraEnv) \(key, value) -> do
echo (" " <> sign (key <> "=" <> value))
forM_ envContentDiff \EnvVarDiff{..} -> do
text <- renderText envValueDiff
echo (" " <> envKey <> "=" <> text)
renderText :: TextDiff -> Render Text
renderText (TextDiff chunks) = do
RenderContext{ indent, orientation, tty, context } <- ask
let n = fromIntegral indent
let prefix = Text.replicate n " "
let format text =
if 80 <= n + Text.length text
then "''\n" <> indentedText <> prefix <> "''"
else text
where
indentedText =
(Text.unlines . fmap indentLine . Text.lines) text
where
indentLine line = prefix <> " " <> line
let renderChunk (Patience.Old l ) =
redBackground orientation tty l
renderChunk (Patience.New r) =
greenBackground orientation tty r
renderChunk (Patience.Both l _) =
grey orientation tty l
let windowedChunks = case context of
Nothing -> chunks
Just m -> fmap middle (filter predicate (zippers chunks))
where
notBoth (Patience.Both _ _) = False
notBoth _ = True
nat = fromIntegral m
predicate (before, line, after) =
any notBoth (line : take nat before ++ take nat after)
middle (_, line, _) = line
return (format (Text.concat (fmap renderChunk windowedChunks)))
ifExist m l = maybe (pure ()) l m
zippers :: [a] -> [([a], a, [a])]
zippers = go []
where
go _ [] = []
go prefix (x : xs) = (prefix, x, xs) : go (x : prefix) xs