vcs-ignore-0.1.0.0: test/Data/VCS/Ignore/Git/CompatibilitySpec.hs
{-# LANGUAGE OverloadedStrings #-}
module Data.VCS.Ignore.Git.CompatibilitySpec (spec) where
import Control.Exception (bracket_)
import Control.Monad (unless)
import Data.VCS.Ignore (
GitRepository,
PathKind (..),
isIgnored,
openGitRepository,
)
import System.Directory (createDirectoryIfMissing)
import System.Environment (
lookupEnv,
setEnv,
unsetEnv,
)
import System.Exit (ExitCode (..))
import System.FilePath (
takeDirectory,
(</>),
)
import System.IO.Temp (withSystemTempDirectory)
import System.Process (readProcessWithExitCode)
import Test.Hspec
spec :: Spec
spec = describe "Git compatibility" $ do
gitExample "matches Git for ordered, anchored, recursive, and escaped rules" $ \root repository -> do
writeText
(root </> ".gitignore")
( unlines
[ "*.log"
, "!keep.log"
, "keep.log"
, "/root.txt"
, "artifacts/**/result.bin"
, "cache/**"
, "file?.[ch]"
, "docs/[a-c].md"
, "build/"
, "\\#literal"
, "\\!important"
]
)
assertMatchesGit repository root RegularFile "drop.log" "drop.log"
assertMatchesGit repository root RegularFile "keep.log" "keep.log"
assertMatchesGit repository root RegularFile "root.txt" "root.txt"
assertMatchesGit repository root RegularFile "nested/root.txt" "nested/root.txt"
assertMatchesGit repository root RegularFile "artifacts/a/b/result.bin" "artifacts/a/b/result.bin"
assertMatchesGit repository root RegularFile "cache/a/b/value" "cache/a/b/value"
assertMatchesGit repository root RegularFile "file1.c" "file1.c"
assertMatchesGit repository root RegularFile "file10.c" "file10.c"
assertMatchesGit repository root RegularFile "docs/b.md" "docs/b.md"
assertMatchesGit repository root Directory "build" "build/"
assertMatchesGit repository root RegularFile "#literal" "#literal"
assertMatchesGit repository root RegularFile "!important" "!important"
gitExample "keeps descendants ignored when their parent is excluded" $ \root repository -> do
writeText (root </> ".gitignore") "build/\n"
writeText (root </> "build" </> ".gitignore") "!keep.txt\n"
assertMatchesGit repository root Directory "build" "build/"
assertMatchesGit repository root RegularFile "build/keep.txt" "build/keep.txt"
gitExample "allows re-inclusion while the parent directory remains visible" $ \root repository -> do
writeText (root </> ".gitignore") "build/*.txt\n!build/keep.txt\n"
assertMatchesGit repository root Directory "build" "build/"
assertMatchesGit repository root RegularFile "build/drop.txt" "build/drop.txt"
assertMatchesGit repository root RegularFile "build/keep.txt" "build/keep.txt"
gitExample "does not extend a parent wildcard below a re-included directory" $ \root repository -> do
writeText (root </> ".gitignore") "foo/*\n!foo/bar/\n"
assertMatchesGit repository root Directory "foo/bar" "foo/bar/"
assertMatchesGit repository root RegularFile "foo/bar/file.txt" "foo/bar/file.txt"
gitExample "treats Glob number ranges as Git literals" $ \root repository -> do
writeText (root </> ".gitignore") "value<1-3>.txt\n"
assertMatchesGit repository root RegularFile "value1.txt" "value1.txt"
assertMatchesGit repository root RegularFile "value<1-3>.txt" "value<1-3>.txt"
gitExample "preserves an escaped hyphen inside a character class" $ \root repository -> do
writeText (root </> ".gitignore") "[a\\-c].txt\n"
assertMatchesGit repository root RegularFile "a.txt" "a.txt"
assertMatchesGit repository root RegularFile "-.txt" "-.txt"
assertMatchesGit repository root RegularFile "b.txt" "b.txt"
assertMatchesGit repository root RegularFile "c.txt" "c.txt"
assertMatchesGit :: GitRepository -> FilePath -> PathKind -> FilePath -> FilePath -> Expectation
assertMatchesGit repository root kind repositoryPath gitPath = do
actual <- isIgnored repository kind repositoryPath
expected <- gitIgnores root gitPath
unless
(actual == expected)
( expectationFailure $
mconcat
[ "mismatch for "
, show repositoryPath
, " (Git path "
, show gitPath
, "): library="
, show actual
, ", Git="
, show expected
]
)
gitIgnores :: FilePath -> FilePath -> IO Bool
gitIgnores root path = do
(exitCode, _, _) <-
readProcessWithExitCode
"git"
[ "-C"
, root
, "check-ignore"
, "--no-index"
, "--quiet"
, "--"
, path
]
""
case exitCode of
ExitSuccess -> pure True
ExitFailure 1 -> pure False
ExitFailure code -> expectationFailure ("git check-ignore failed with " <> show code) >> pure False
gitExample :: String -> (FilePath -> GitRepository -> Expectation) -> Spec
gitExample description action =
it description . withSystemTempDirectory "vcs-ignore-git-compatibility" $ \sandbox ->
withEnvironment "XDG_CONFIG_HOME" (sandbox </> "xdg") $ do
let root = sandbox </> "repository"
createDirectoryIfMissing True root
initializeGit root
repository <- openGitRepository root
action root repository
initializeGit :: FilePath -> IO ()
initializeGit root = do
(exitCode, _, errors) <- readProcessWithExitCode "git" ["init", "--quiet", root] ""
case exitCode of
ExitSuccess -> pure ()
ExitFailure code -> expectationFailure $ "git init failed with " <> show code <> ": " <> errors
withEnvironment :: String -> String -> IO a -> IO a
withEnvironment name value action = do
original <- lookupEnv name
bracket_ (setEnv name value) (restore original) action
where
restore Nothing = unsetEnv name
restore (Just originalValue) = setEnv name originalValue
writeText :: FilePath -> String -> IO ()
writeText path content = do
createDirectoryIfMissing True $ takeDirectory path
writeFile path content