gitignore-ospath (empty) → 0.1.0
raw patch · 6 files changed
+767/−0 lines, 6 filesdep +basedep +filepathdep +gitignore-ospath
Dependencies added: base, filepath, gitignore-ospath, hspec, os-string, text
Files
- CHANGELOG.md +5/−0
- LICENSE +201/−0
- gitignore-ospath.cabal +53/−0
- src/Ignore.hs +10/−0
- src/Ignore/Internal.hs +277/−0
- test/IgnoreSpec.hs +221/−0
+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Revision history for gitignore-ospath++## 0.1.0 -- 2026-01-02++* First version. Released on an unsuspecting world.
+ LICENSE view
@@ -0,0 +1,201 @@+ Apache License+ Version 2.0, January 2004+ http://www.apache.org/licenses/++ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION++ 1. Definitions.++ "License" shall mean the terms and conditions for use, reproduction,+ and distribution as defined by Sections 1 through 9 of this document.++ "Licensor" shall mean the copyright owner or entity authorized by+ the copyright owner that is granting the License.++ "Legal Entity" shall mean the union of the acting entity and all+ other entities that control, are controlled by, or are under common+ control with that entity. For the purposes of this definition,+ "control" means (i) the power, direct or indirect, to cause the+ direction or management of such entity, whether by contract or+ otherwise, or (ii) ownership of fifty percent (50%) or more of the+ outstanding shares, or (iii) beneficial ownership of such entity.++ "You" (or "Your") shall mean an individual or Legal Entity+ exercising permissions granted by this License.++ "Source" form shall mean the preferred form for making modifications,+ including but not limited to software source code, documentation+ source, and configuration files.++ "Object" form shall mean any form resulting from mechanical+ transformation or translation of a Source form, including but+ not limited to compiled object code, generated documentation,+ and conversions to other media types.++ "Work" shall mean the work of authorship, whether in Source or+ Object form, made available under the License, as indicated by a+ copyright notice that is included in or attached to the work+ (an example is provided in the Appendix below).++ "Derivative Works" shall mean any work, whether in Source or Object+ form, that is based on (or derived from) the Work and for which the+ editorial revisions, annotations, elaborations, or other modifications+ represent, as a whole, an original work of authorship. For the purposes+ of this License, Derivative Works shall not include works that remain+ separable from, or merely link (or bind by name) to the interfaces of,+ the Work and Derivative Works thereof.++ "Contribution" shall mean any work of authorship, including+ the original version of the Work and any modifications or additions+ to that Work or Derivative Works thereof, that is intentionally+ submitted to Licensor for inclusion in the Work by the copyright owner+ or by an individual or Legal Entity authorized to submit on behalf of+ the copyright owner. For the purposes of this definition, "submitted"+ means any form of electronic, verbal, or written communication sent+ to the Licensor or its representatives, including but not limited to+ communication on electronic mailing lists, source code control systems,+ and issue tracking systems that are managed by, or on behalf of, the+ Licensor for the purpose of discussing and improving the Work, but+ excluding communication that is conspicuously marked or otherwise+ designated in writing by the copyright owner as "Not a Contribution."++ "Contributor" shall mean Licensor and any individual or Legal Entity+ on behalf of whom a Contribution has been received by Licensor and+ subsequently incorporated within the Work.++ 2. Grant of Copyright License. Subject to the terms and conditions of+ this License, each Contributor hereby grants to You a perpetual,+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable+ copyright license to reproduce, prepare Derivative Works of,+ publicly display, publicly perform, sublicense, and distribute the+ Work and such Derivative Works in Source or Object form.++ 3. Grant of Patent License. Subject to the terms and conditions of+ this License, each Contributor hereby grants to You a perpetual,+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable+ (except as stated in this section) patent license to make, have made,+ use, offer to sell, sell, import, and otherwise transfer the Work,+ where such license applies only to those patent claims licensable+ by such Contributor that are necessarily infringed by their+ Contribution(s) alone or by combination of their Contribution(s)+ with the Work to which such Contribution(s) was submitted. If You+ institute patent litigation against any entity (including a+ cross-claim or counterclaim in a lawsuit) alleging that the Work+ or a Contribution incorporated within the Work constitutes direct+ or contributory patent infringement, then any patent licenses+ granted to You under this License for that Work shall terminate+ as of the date such litigation is filed.++ 4. Redistribution. You may reproduce and distribute copies of the+ Work or Derivative Works thereof in any medium, with or without+ modifications, and in Source or Object form, provided that You+ meet the following conditions:++ (a) You must give any other recipients of the Work or+ Derivative Works a copy of this License; and++ (b) You must cause any modified files to carry prominent notices+ stating that You changed the files; and++ (c) You must retain, in the Source form of any Derivative Works+ that You distribute, all copyright, patent, trademark, and+ attribution notices from the Source form of the Work,+ excluding those notices that do not pertain to any part of+ the Derivative Works; and++ (d) If the Work includes a "NOTICE" text file as part of its+ distribution, then any Derivative Works that You distribute must+ include a readable copy of the attribution notices contained+ within such NOTICE file, excluding those notices that do not+ pertain to any part of the Derivative Works, in at least one+ of the following places: within a NOTICE text file distributed+ as part of the Derivative Works; within the Source form or+ documentation, if provided along with the Derivative Works; or,+ within a display generated by the Derivative Works, if and+ wherever such third-party notices normally appear. The contents+ of the NOTICE file are for informational purposes only and+ do not modify the License. You may add Your own attribution+ notices within Derivative Works that You distribute, alongside+ or as an addendum to the NOTICE text from the Work, provided+ that such additional attribution notices cannot be construed+ as modifying the License.++ You may add Your own copyright statement to Your modifications and+ may provide additional or different license terms and conditions+ for use, reproduction, or distribution of Your modifications, or+ for any such Derivative Works as a whole, provided Your use,+ reproduction, and distribution of the Work otherwise complies with+ the conditions stated in this License.++ 5. Submission of Contributions. Unless You explicitly state otherwise,+ any Contribution intentionally submitted for inclusion in the Work+ by You to the Licensor shall be under the terms and conditions of+ this License, without any additional terms or conditions.+ Notwithstanding the above, nothing herein shall supersede or modify+ the terms of any separate license agreement you may have executed+ with Licensor regarding such Contributions.++ 6. Trademarks. This License does not grant permission to use the trade+ names, trademarks, service marks, or product names of the Licensor,+ except as required for reasonable and customary use in describing the+ origin of the Work and reproducing the content of the NOTICE file.++ 7. Disclaimer of Warranty. Unless required by applicable law or+ agreed to in writing, Licensor provides the Work (and each+ Contributor provides its Contributions) on an "AS IS" BASIS,+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or+ implied, including, without limitation, any warranties or conditions+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A+ PARTICULAR PURPOSE. You are solely responsible for determining the+ appropriateness of using or redistributing the Work and assume any+ risks associated with Your exercise of permissions under this License.++ 8. Limitation of Liability. In no event and under no legal theory,+ whether in tort (including negligence), contract, or otherwise,+ unless required by applicable law (such as deliberate and grossly+ negligent acts) or agreed to in writing, shall any Contributor be+ liable to You for damages, including any direct, indirect, special,+ incidental, or consequential damages of any character arising as a+ result of this License or out of the use or inability to use the+ Work (including but not limited to damages for loss of goodwill,+ work stoppage, computer failure or malfunction, or any and all+ other commercial damages or losses), even if such Contributor+ has been advised of the possibility of such damages.++ 9. Accepting Warranty or Additional Liability. While redistributing+ the Work or Derivative Works thereof, You may choose to offer,+ and charge a fee for, acceptance of support, warranty, indemnity,+ or other liability obligations and/or rights consistent with this+ License. However, in accepting such obligations, You may act only+ on Your own behalf and on Your sole responsibility, not on behalf+ of any other Contributor, and only if You agree to indemnify,+ defend, and hold each Contributor harmless for any liability+ incurred by, or claims asserted against, such Contributor by reason+ of your accepting any such warranty or additional liability.++ END OF TERMS AND CONDITIONS++ APPENDIX: How to apply the Apache License to your work.++ To apply the Apache License to your work, attach the following+ boilerplate notice, with the fields enclosed by brackets "[]"+ replaced with your own identifying information. (Don't include+ the brackets!) The text should be enclosed in the appropriate+ comment syntax for the file format. We also recommend that a+ file or class name and description of purpose be included on the+ same "printed page" as the copyright notice for easier+ identification within third-party archives.++ Copyright [yyyy] [name of copyright owner]++ Licensed under the Apache License, Version 2.0 (the "License");+ you may not use this file except in compliance with the License.+ You may obtain a copy of the License at++ http://www.apache.org/licenses/LICENSE-2.0++ Unless required by applicable law or agreed to in writing, software+ distributed under the License is distributed on an "AS IS" BASIS,+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ See the License for the specific language governing permissions and+ limitations under the License.
+ gitignore-ospath.cabal view
@@ -0,0 +1,53 @@+cabal-version: 3.0+name: gitignore-ospath+version: 0.1.0+synopsis: A compact Haskell library for reading `.gitignore` files and filtering paths.+description: A compact Haskell library for reading `.gitignore` files and filtering paths.+ .+ The library works with `.gitignore`-like formats (e.g., `.dockerignore`) and uses+ 'System.OsPath.OsPath' for path handling. Parsing and filtering rules follow the+ Git documentation.+homepage: https://github.com/federicotdn/gitignore-ospath+bug-reports: https://github.com/federicotdn/gitignore-ospath/issues+license: Apache-2.0+license-file: LICENSE+author: Federico Tedin+maintainer: federicotedin@gmail.com+category: Development+build-type: Simple+extra-doc-files: CHANGELOG.md++source-repository head+ type: git+ location: https://github.com/federicotdn/gitignore-ospath++common warnings+ ghc-options: -Wall++common shared-deps+ default-language: Haskell2010+ build-depends: base >=4.14.0.0 && <5+ , os-string >=2.0.0 && < 3+ , filepath >=1.5.0.0 && < 2+ , text >=2.1.0 && < 3+ default-extensions: OverloadedStrings++library+ import: warnings+ , shared-deps+ exposed-modules: Ignore+ other-modules: Ignore.Internal+ hs-source-dirs: src++test-suite gitignore-ospath-test+ import: warnings+ , shared-deps+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ , src+ main-is: IgnoreSpec.hs+ other-modules: Ignore+ , Ignore.Internal+ build-depends: base >=4.14.0.0 && <5+ , hspec ^>=2.11.16+ , gitignore-ospath
+ src/Ignore.hs view
@@ -0,0 +1,10 @@+-- | Gitignore pattern matching for 'System.OsPath.OsPath'.+module Ignore+ ( Ignore (..),+ parse,+ ignores,+ ignores',+ )+where++import Ignore.Internal
+ src/Ignore/Internal.hs view
@@ -0,0 +1,277 @@+module Ignore.Internal where++import Data.List (tails)+import Data.Maybe (isJust, mapMaybe)+import Data.Text (Text)+import qualified Data.Text as T+import GHC.IO.Encoding (utf16le, utf8)+import System.OsPath (OsPath, splitDirectories)+import System.OsString (OsChar, encodeWith)+import qualified System.OsString as OS+import Text.ParserCombinators.ReadP (ReadP)+import qualified Text.ParserCombinators.ReadP as R++data GlobClassPart+ = ClassSingle OsChar+ | ClassRange (OsChar, OsChar)+ | ClassSep+ deriving (Show, Eq)++data GlobPart+ = Wildcard Bool+ | Single OsChar+ | Class [GlobClassPart]+ | Noop+ deriving (Show, Eq)++data Segment+ = DAsterisk+ | Asterisk+ | Literal OsPath+ | Prefix OsPath+ | Suffix OsPath+ | Glob [GlobPart]+ deriving (Show, Eq)++data Pattern = Pattern+ { pSegments :: [Segment],+ pDir :: Bool,+ pNegated :: Bool,+ pAnchored :: Bool+ }+ deriving (Show, Eq)++-- | The parsed contents of a gitignore file.+--+-- Multiple 'Ignore' values can be combined using '<>' to merge their patterns.+-- This is useful for combining patterns from multiple gitignore files (e.g.,+-- a global gitignore and a repository-specific one). Later patterns take+-- precedence when determining if a path should be ignored.+newtype Ignore = Ignore [Pattern] deriving (Show, Eq)++-- | Combine gitignore files by appending their patterns.+--+-- When combining @ignore1 <> ignore2@, patterns from @ignore2@ are appended+-- after @ignore1@, meaning later patterns take precedence for matching.+instance Semigroup Ignore where+ (Ignore p1) <> (Ignore p2) = Ignore (p1 <> p2)++-- TODO: This is probably overkill for encoding just one char.+encodeChar :: Char -> Maybe OsChar+encodeChar ch =+ let str = [ch]+ encoded = OS.unpack <$> encodeWith utf8 utf16le str+ in case encoded of+ Left _ -> Nothing+ Right [] -> Nothing+ Right (osch : _) -> Just osch++getOsChar :: ReadP (Char, OsChar)+getOsChar = do+ ch <- R.get+ let osch = encodeChar ch+ case osch of+ Just osch' -> pure (ch, osch')+ Nothing -> R.pfail++finishRange :: (OsChar, OsChar) -> [GlobClassPart]+finishRange (start, end) =+ if start < end+ then [ClassRange (start, end), ClassSep]+ else [ClassSingle start, ClassSep]++parseGlobInner :: [GlobPart] -> Maybe [GlobClassPart] -> ReadP [GlobPart]+parseGlobInner curr class_ = do+ atEnd <- null <$> R.look+ case class_ of+ -- We are outside a class definition e.g. ab|cd[efg]+ Nothing ->+ if atEnd+ then return curr -- Finished!+ else do+ (ch, osch) <- getOsChar+ case ch of+ '[' -> contWith Noop (Just []) -- Start building a class.+ '?' -> contWith (Wildcard False) Nothing+ '*' -> contWith (Wildcard True) Nothing+ '\\' -> do+ -- Maybe read one more char.+ (_, osch') <- getOsChar+ contWith (Single osch') Nothing+ _ -> contWith (Single osch) Nothing -- Includes ']' case.++ -- We are inside a class definition e.g. abcd[e|fg]+ Just class_' -> do+ -- The line below will fail if there's nothing to read, i.e.+ -- the class was opened but never closed.+ (ch, osch) <- getOsChar+ case ch of+ ']' -> do+ let classParts = filter (/= ClassSep) class_'+ contWith (if null classParts then Noop else Class classParts) Nothing+ '-' -> do+ case class_' of+ [] -> contWith Noop $ Just [ClassSingle osch]+ elems -> case last elems of+ ClassRange (start, _) -> contWith Noop $ Just (init class_' ++ finishRange (start, osch))+ -- Start building a range.+ ClassSingle cs -> contWith Noop $ Just (init class_' ++ [ClassRange (cs, cs)])+ ClassSep -> contWith Noop $ Just (class_' ++ [ClassSingle osch]) -- Just add '-'.+ _ -> do+ -- Maybe read one more char.+ (_, osch') <- if ch == '\\' then getOsChar else pure (ch, osch)+ case class_' of+ [] -> contWith Noop $ Just [ClassSingle osch']+ elems -> case last elems of+ ClassRange (start, _) -> contWith Noop $ Just (init class_' ++ finishRange (start, osch'))+ _ -> contWith Noop $ Just (class_' ++ [ClassSingle osch'])+ where+ contWith p = parseGlobInner (curr ++ [p])++parseGlob :: Text -> Maybe Segment+parseGlob source = do+ let result = R.readP_to_S (parseGlobInner [] Nothing) (T.unpack source)+ case result of+ [] -> Nothing+ (r : _) -> do+ let parts = filter (/= Noop) $ fst r+ case parts of+ [] -> Nothing+ _ -> Just (Glob parts)++parseSegment :: Text -> Maybe Segment+parseSegment source+ | source == "*" = Just Asterisk+ | T.length source == acount = Just DAsterisk -- '**' or more asterisks+ | scount == 0 && acount == 1 && aprefix = Suffix . OS.tail <$> encoded+ | scount == 0 && acount == 1 && asuffix = Prefix . OS.init <$> encoded+ | scount > 0 || acount > 0 = parseGlob source+ | otherwise = Literal <$> encoded+ where+ aprefix = "*" `T.isPrefixOf` source+ asuffix = "*" `T.isSuffixOf` source+ acount = T.count "*" source+ scount = T.length (T.filter (`elem` ['[', ']', '?', '\\']) source)+ encoded = either (const Nothing) Just $ encodeWith utf8 utf16le (T.unpack source)++parsePattern :: Text -> Maybe Pattern+parsePattern source =+ let (source', negated) =+ if not (T.null source) && T.head source == '!'+ then (T.tail source, True)+ else (source, False)+ segments = filter (not . T.null) $ T.splitOn "/" source'+ segments' = mapMaybe parseSegment segments+ dir = "/" `T.isSuffixOf` source'+ anchored = "/" `T.isPrefixOf` source' || length segments' > 1+ in if null segments'+ then Nothing+ else+ Just+ Pattern+ { pSegments = segments',+ pDir = dir,+ pNegated = negated,+ pAnchored = anchored+ }++-- | Parse a complete gitignore file content into an 'Ignore' collection.+--+-- Strips whitespace, removes comments (lines starting with @#@), and parses+-- each remaining line as a pattern.+parse :: Text -> Ignore+parse source =+ let sourceLines = filter (not . T.null) $ map T.strip $ T.lines source+ patterns = filter (\l -> T.head l /= '#') sourceLines+ in Ignore (mapMaybe parsePattern patterns)++globClassMatches :: OsChar -> GlobClassPart -> Bool+globClassMatches curr part =+ case part of+ ClassSingle osch -> osch == curr+ ClassRange (start, end) -> (start <= curr) && (curr <= end)+ ClassSep -> False++matchesEmpty :: GlobPart -> Bool+matchesEmpty (Wildcard True) = True+matchesEmpty Noop = True+matchesEmpty _ = False++globMatches :: [GlobPart] -> OsPath -> Bool+globMatches parts path =+ if OS.null path+ then null parts || all matchesEmpty parts+ else case parts of+ [] -> False+ (part : rest) ->+ let pathTail = OS.tail path+ cont = globMatches rest pathTail+ curr = OS.head path+ in case part of+ Wildcard True -> cont || globMatches parts pathTail || globMatches rest path+ Wildcard False -> cont+ Single osch -> if osch == curr then cont else False+ Noop -> globMatches rest path+ Class classParts ->+ if any (globClassMatches curr) classParts+ then cont+ else False++segmentMatches :: Segment -> OsPath -> Bool+segmentMatches target path = case target of+ Asterisk -> True+ Literal val -> path == val+ Suffix val -> val `OS.isSuffixOf` path+ Prefix val -> val `OS.isPrefixOf` path+ Glob parts -> globMatches parts path+ -- NOTE: DAsterisk is already handled in 'patternIgnoresInner'.+ DAsterisk -> error "Unhandled ** pattern"++patternIgnoresInner :: Pattern -> [OsPath] -> Maybe Bool+patternIgnoresInner pat@Pattern {pNegated = True} splitPath =+ not <$> patternIgnoresInner pat {pNegated = False} splitPath+patternIgnoresInner pat@Pattern {pSegments = segs} splitPath =+ case splitPath of+ [] ->+ -- Exhausted path without returning False, meaning we might+ -- have an ignore-match. This depends on whether there is more+ -- pattern to match.+ if null segs then Just True else Nothing+ (pathHead : pathTail) -> case segs of+ [] -> Nothing+ (DAsterisk : rest) ->+ if null rest+ then Just True+ else+ let matches = mapMaybe (patternIgnoresInner (pat {pSegments = rest})) (init $ tails splitPath)+ in if null matches then Nothing else Just (or matches)+ (seg : rest) ->+ let match = segmentMatches seg pathHead+ continued = patternIgnoresInner (pat {pSegments = rest}) pathTail+ retry = if null pathTail then Nothing else patternIgnoresInner pat pathTail+ in if pAnchored pat+ then if match then continued else Nothing+ else (if match && isJust continued then continued else retry)++patternIgnores :: Pattern -> [OsPath] -> Bool -> Maybe Bool+patternIgnores pat splitPath dir =+ if (pDir pat && not dir) || null splitPath+ then Nothing+ else patternIgnoresInner pat splitPath++ignoresInner :: Ignore -> [OsPath] -> Bool -> Bool+ignoresInner (Ignore patterns) splitPath dir =+ last $ False : mapMaybe (\pat -> patternIgnores pat splitPath dir) patterns++-- | Check if an 'Ignore' collection ignores the given path.+--+-- The third parameter indicates whether the path is a directory.+ignores :: Ignore -> OsPath -> Bool -> Bool+ignores ign path = ignoresInner ign (splitDirectories path)++-- | Check if an 'Ignore' collection ignores the given pre-split path.+--+-- Like 'ignores', but takes a path already split into components. The third+-- parameter indicates whether the path is a directory.+ignores' :: Ignore -> [OsPath] -> Bool -> Bool+ignores' = ignoresInner
+ test/IgnoreSpec.hs view
@@ -0,0 +1,221 @@+module Main (main) where++import Data.Text (Text)+import qualified Data.Text as T+import Ignore+import Ignore.Internal+import System.OsString (OsChar, OsString, unsafeEncodeUtf, unsafeFromChar)+import Test.Hspec++os :: String -> OsString+os = unsafeEncodeUtf++osc :: Char -> OsChar+osc = unsafeFromChar++main :: IO ()+main = hspec spec++path :: String -> Segment+path p = Literal (os p)++parseln :: [Text] -> Ignore+parseln patterns = parse (T.intercalate "\n" patterns)++pat :: [Segment] -> Bool -> Bool -> Bool -> Pattern+pat segs dir negated anchored =+ Pattern+ { pSegments = segs,+ pDir = dir,+ pNegated = negated,+ pAnchored = anchored+ }++spec :: Spec+spec = do+ describe "parse" $ do+ it "parses an ignore correctly" $ do+ parse "" `shouldBe` Ignore []+ parse "\n \n\t\t\n \n" `shouldBe` Ignore []+ parse "# test" `shouldBe` Ignore []+ parse " # test " `shouldBe` Ignore []+ parse " # test\n#test" `shouldBe` Ignore []+ parse "path" `shouldBe` Ignore [pat [path "path"] False False False]+ parse "!path" `shouldBe` Ignore [pat [path "path"] False True False]+ parse "!!path" `shouldBe` Ignore [pat [path "!path"] False True False]+ parse "path/" `shouldBe` Ignore [pat [path "path"] True False False]+ parse "path///" `shouldBe` Ignore [pat [path "path"] True False False]+ parse "/path/" `shouldBe` Ignore [pat [path "path"] True False True]+ parse "/path/foo/" `shouldBe` Ignore [pat [path "path", path "foo"] True False True]+ parse "/path///foo/" `shouldBe` Ignore [pat [path "path", path "foo"] True False True]+ parse "**/path/" `shouldBe` Ignore [pat [DAsterisk, path "path"] True False True]+ parse "/path/**" `shouldBe` Ignore [pat [path "path", DAsterisk] False False True]+ parse "/path/**/" `shouldBe` Ignore [pat [path "path", DAsterisk] True False True]+ parse "a?" `shouldBe` Ignore [pat [Glob [Single (osc 'a'), Wildcard False]] False False False]+ parse "*a*" `shouldBe` Ignore [pat [Glob [Wildcard True, Single (osc 'a'), Wildcard True]] False False False]+ parse "a]" `shouldBe` Ignore [pat [Glob [Single (osc 'a'), Single (osc ']')]] False False False]+ parse "\\a" `shouldBe` Ignore [pat [Glob [Single (osc 'a')]] False False False]+ parse "a\\?" `shouldBe` Ignore [pat [Glob [Single (osc 'a'), Single (osc '?')]] False False False]+ parse "[ab]" `shouldBe` Ignore [pat [Glob [Class [ClassSingle (osc 'a'), ClassSingle (osc 'b')]]] False False False]+ parse "[a\\b]" `shouldBe` Ignore [pat [Glob [Class [ClassSingle (osc 'a'), ClassSingle (osc 'b')]]] False False False]+ parse "[a\\]]" `shouldBe` Ignore [pat [Glob [Class [ClassSingle (osc 'a'), ClassSingle (osc ']')]]] False False False]+ parse "[-]" `shouldBe` Ignore [pat [Glob [Class [ClassSingle (osc '-')]]] False False False]+ parse "[--]" `shouldBe` Ignore [pat [Glob [Class [ClassRange (osc '-', osc '-')]]] False False False]+ parse "[---]" `shouldBe` Ignore [pat [Glob [Class [ClassSingle (osc '-')]]] False False False]+ parse "[a-]" `shouldBe` Ignore [pat [Glob [Class [ClassRange (osc 'a', osc 'a')]]] False False False]+ parse "[a-z]" `shouldBe` Ignore [pat [Glob [Class [ClassRange (osc 'a', osc 'z')]]] False False False]+ parse "[a--z]" `shouldBe` Ignore [pat [Glob [Class [ClassSingle (osc 'a'), ClassSingle (osc 'z')]]] False False False]+ parse "[1a-z]" `shouldBe` Ignore [pat [Glob [Class [ClassSingle (osc '1'), ClassRange (osc 'a', osc 'z')]]] False False False]+ parse "[a-z2]" `shouldBe` Ignore [pat [Glob [Class [ClassRange (osc 'a', osc 'z'), ClassSingle (osc '2')]]] False False False]+ parse "[a-zA-Z]" `shouldBe` Ignore [pat [Glob [Class [ClassRange (osc 'a', osc 'z'), ClassRange (osc 'A', osc 'Z')]]] False False False]+ parse "[-a-z]" `shouldBe` Ignore [pat [Glob [Class [ClassSingle (osc '-'), ClassRange (osc 'a', osc 'z')]]] False False False]+ parse "[a-z-]" `shouldBe` Ignore [pat [Glob [Class [ClassRange (osc 'a', osc 'z'), ClassSingle (osc '-')]]] False False False]+ parse "[z-a]" `shouldBe` Ignore [pat [Glob [Class [ClassSingle (osc 'z')]]] False False False]+ parse "[z-z]" `shouldBe` Ignore [pat [Glob [Class [ClassSingle (osc 'z')]]] False False False]+ parse "[z-a-m]" `shouldBe` Ignore [pat [Glob [Class [ClassSingle (osc 'z'), ClassSingle (osc '-'), ClassSingle (osc 'm')]]] False False False]+ parse "[]]" `shouldBe` Ignore [pat [Glob [Single (osc ']')]] False False False]+ parse "[[]" `shouldBe` Ignore [pat [Glob [Class [ClassSingle (osc '[')]]] False False False]+ parse "a[" `shouldBe` Ignore [] -- Unclosed class+ parse "a[b" `shouldBe` Ignore [] -- Unclosed class+ parse "[b[" `shouldBe` Ignore [] -- Unclosed class+ parse "[]" `shouldBe` Ignore [] -- Empty+ parse "[a\\]" `shouldBe` Ignore [] -- Unclosed class+ parse "\\" `shouldBe` Ignore []++ describe "ignores" $ do+ it "ignores paths correctly (single pattern)" $ do+ ignores (parse "") (os "") False `shouldBe` False+ ignores (parse "") (os "foo") False `shouldBe` False+ ignores (parse "!") (os "foo") False `shouldBe` False+ ignores (parse "!*") (os "foo") False `shouldBe` False+ ignores (parse "bar") (os "foo") False `shouldBe` False+ ignores (parse "!bar") (os "foo") False `shouldBe` False+ ignores (parse "foo") (os "foo") False `shouldBe` True+ ignores (parse "/") (os "foo") False `shouldBe` False+ ignores (parse "/") (os "foo") True `shouldBe` False+ ignores (parse "/") (os "") False `shouldBe` False+ ignores (parse "!foo") (os "foo") False `shouldBe` False+ ignores (parse "!!foo") (os "foo") False `shouldBe` False+ ignores (parse "foo") (os "foo") True `shouldBe` True+ ignores (parse "foo/") (os "foo") False `shouldBe` False+ ignores (parse "foo/") (os "foo") True `shouldBe` True+ ignores (parse "/foo") (os "foo") False `shouldBe` True+ ignores (parse "foo/bar") (os "foo/bar") False `shouldBe` True+ ignores (parse "foo/bar") (os "foo/baz") False `shouldBe` False+ ignores (parse "/foo/bar") (os "foo/bar") False `shouldBe` True+ ignores (parse "/foo/bar") (os "baz/foo/bar") False `shouldBe` False+ ignores (parse "bar") (os "foo/bar") False `shouldBe` True+ ignores (parse "bar/") (os "foo/bar") True `shouldBe` True+ ignores (parse "/bar") (os "foo/bar") False `shouldBe` False+ ignores (parse "foo/bar") (os "baz/foo/bar") False `shouldBe` False+ ignores (parse "*") (os "foo") False `shouldBe` True+ ignores (parse "*") (os "foo") True `shouldBe` True+ ignores (parse "*") (os "foo/bar/baz") False `shouldBe` True+ ignores (parse "/*") (os "foo") False `shouldBe` True+ ignores (parse "/*") (os "foo/bar") False `shouldBe` False+ ignores (parse "*/*") (os "foo/bar") False `shouldBe` True+ ignores (parse "*/*") (os "foo/bar/baz") False `shouldBe` False+ ignores (parse "*/*") (os "foo") False `shouldBe` False+ ignores (parse "foo/bar") (os "foo/x/bar") False `shouldBe` False+ ignores (parse "foo/*/bar") (os "foo/x/bar") False `shouldBe` True+ ignores (parse "foo/*/bar") (os "foo/x/baz") False `shouldBe` False+ ignores (parse "foo/*/bar") (os "foo/x") False `shouldBe` False+ ignores (parse "**") (os "foo") False `shouldBe` True+ ignores (parse "**") (os "foo/bar/baz") False `shouldBe` True+ ignores (parse "**/bar") (os "x/foo/bar") False `shouldBe` True+ ignores (parse "**/foo/bar") (os "x/foo/bar") False `shouldBe` True+ ignores (parse "**/foo/baz") (os "x/foo/bar") False `shouldBe` False+ ignores (parse "bar/**") (os "bar/a") False `shouldBe` True+ ignores (parse "bar/***") (os "bar/a") False `shouldBe` True+ ignores (parse "*/**") (os "bar/a") False `shouldBe` True+ ignores (parse "bar/**") (os "bar") False `shouldBe` False+ ignores (parse "bar/**/x") (os "bar/x") False `shouldBe` True+ ignores (parse "bar/**/x") (os "bar/1/2/3/x") False `shouldBe` True+ ignores (parse "bar/**/x/") (os "bar/1/2/3/x") False `shouldBe` False+ ignores (parse "bar/**/x/") (os "bar/1/2/3/x") True `shouldBe` True+ ignores (parse "bar/***/x/") (os "bar/1/2/3/x") True `shouldBe` True+ ignores (parse "bar/**/") (os "bar/test") True `shouldBe` True+ ignores (parse "bar/**/") (os "bar/test") False `shouldBe` False+ ignores (parse "**/x") (os "foo") False `shouldBe` False+ ignores (parse "**/**") (os "foo") False `shouldBe` True+ ignores (parse "**/**") (os "foo/bar") False `shouldBe` True+ ignores (parse "!**/foo") (os "bar/foo") False `shouldBe` False+ ignores (parse "a/**/b/**/c") (os "a/x/b/y/c") False `shouldBe` True+ ignores (parse "a/**/m/**/c") (os "a/x/b/y/c") False `shouldBe` False+ ignores (parse "/**") (os "foo") False `shouldBe` True+ ignores (parse "/**") (os "foo/yyy") False `shouldBe` True+ ignores (parse "/**/foo") (os "foo") False `shouldBe` True+ ignores (parse "/**/foo") (os "x/foo") False `shouldBe` True+ ignores (parse "/**/foo") (os "x/y/foo") False `shouldBe` True+ ignores (parse "/***/foo") (os "x/y/foo") False `shouldBe` True+ ignores (parse "!foo/") (os "foo") True `shouldBe` False+ ignores (parse "!foo") (os "foo") True `shouldBe` False+ ignores (parse ".*") (os "foo") False `shouldBe` False+ ignores (parse ".*") (os ".foo") False `shouldBe` True+ ignores (parse ".*") (os "bar/.foo") False `shouldBe` True+ ignores (parse ".*") (os "bar/.") False `shouldBe` True+ ignores (parse "*.py") (os "test.py") False `shouldBe` True+ ignores (parse "*.py") (os "test.c") False `shouldBe` False+ ignores (parse "*.py") (os "foo/test.py") False `shouldBe` True+ ignores (parse "foo/*.py") (os "test.py") False `shouldBe` False+ ignores (parse "foo/*.py") (os "foo/test.py") False `shouldBe` True+ ignores (parse "**/*.py") (os "a/b/foo/test.py") False `shouldBe` True+ ignores (parse "**/*.py") (os "test.py") False `shouldBe` True+ ignores (parse "**/") (os "test") True `shouldBe` True+ ignores (parse "**/") (os "test/dir/foo") True `shouldBe` True+ ignores (parse "**/") (os "test/file") False `shouldBe` False+ ignores (parse "/*/") (os "test") True `shouldBe` True+ ignores (parse "/*/") (os "test/foo") True `shouldBe` False+ ignores (parse "**/*.py") (os "zzz") False `shouldBe` False+ ignores (parse "!**/*.py") (os "zzz") False `shouldBe` False+ ignores (parse "/baz/**") (os "a/baz/foo") False `shouldBe` False+ -- Globs+ ignores (parse "??") (os "ab") False `shouldBe` True+ ignores (parse "??") (os "abc") False `shouldBe` False+ ignores (parse "?ab*") (os "aabcd") False `shouldBe` True+ ignores (parse "a*c") (os "abc") False `shouldBe` True+ ignores (parse "a*c") (os "ac") False `shouldBe` True+ ignores (parse "a*cb") (os "acb") False `shouldBe` True+ ignores (parse "a*c") (os "abx") False `shouldBe` False+ ignores (parse "foo]") (os "foo]") False `shouldBe` True+ ignores (parse "\\?") (os "?") False `shouldBe` True+ ignores (parse "[]") (os "a") False `shouldBe` False+ ignores (parse "[-]") (os "-") False `shouldBe` True+ ignores (parse "[ab]") (os "a") False `shouldBe` True+ ignores (parse "[ab]") (os "c") False `shouldBe` False+ ignores (parse "[a-z]") (os "c") False `shouldBe` True+ ignores (parse "[a-z]") (os "0") False `shouldBe` False+ ignores (parse "[a-zA-Z]") (os "A") False `shouldBe` True+ ignores (parse "[z-a]") (os "z") False `shouldBe` True+ ignores (parse "*.?[a-z][-]\\?\\**") (os "abc.xa-?*-") False `shouldBe` True+ ignores (parse "*.?[a-z][-]\\?\\**") (os "abcxa-?*-") False `shouldBe` False++ -- From https://git-scm.com/docs/gitignore:+ ignores (parse "doc/frotz/") (os "doc/frotz") True `shouldBe` True+ ignores (parse "doc/frotz/") (os "a/doc/frotz") True `shouldBe` False+ ignores (parse "frotz/") (os "frotz") True `shouldBe` True+ ignores (parse "frotz/") (os "a/frotz") True `shouldBe` True+ ignores (parse "**/foo") (os "a/foo") False `shouldBe` True+ ignores (parse "**/foo/bar") (os "a/b/foo/bar") False `shouldBe` True+ ignores (parse "abc/**") (os "abc/foo/bar") False `shouldBe` True+ ignores (parse "a/**/b") (os "a/b") False `shouldBe` True+ ignores (parse "a/**/b") (os "a/x/b") False `shouldBe` True+ ignores (parse "a/**/b") (os "a/x/y/b") False `shouldBe` True+ ignores (parse "foo/*") (os "foo/test.json") False `shouldBe` True+ ignores (parse "foo/*") (os "foo/bar") True `shouldBe` True+ ignores (parse "foo/*") (os "foo/bar/hello.c") False `shouldBe` False++ it "ignores paths correctly (multiple patterns)" $ do+ ignores (parseln ["foo", "!foo"]) (os "foo") False `shouldBe` False+ ignores (parseln ["!foo", "!!foo"]) (os "foo") False `shouldBe` False+ ignores (parseln ["*", "!foo"]) (os "foo") False `shouldBe` False+ ignores (parseln ["*", "!*"]) (os "foo") False `shouldBe` False+ ignores (parseln ["*", "!bar"]) (os "foo") False `shouldBe` True+ ignores (parseln ["*", "!foo", "foo"]) (os "foo") False `shouldBe` True+ ignores (parseln ["test/**", "!test/foo"]) (os "test/foo") False `shouldBe` False+ ignores (parseln ["*.py", "!test.py"]) (os "test.py") False `shouldBe` False+ ignores (parseln ["*", "!*/"]) (os "foo/bar") True `shouldBe` False+ ignores (parseln ["*", "!*/"]) (os "foo/bar") False `shouldBe` True+ ignores (parseln ["*", "!*/", "!**/*.py"]) (os "bar.py") False `shouldBe` False+ ignores (parseln ["*", "!*/", "!**/*.py"]) (os "foo/bar.py") False `shouldBe` False+ ignores (parseln ["*", "!*/", "!*.py"]) (os "foo/bar.py") False `shouldBe` False