@@ -7,15 +7,13 @@ import Control.Monad.Maybe.Trans (runMaybeT)
7
7
import Control.Monad.Trans.Class (lift )
8
8
import Data.Array as Array
9
9
import Data.Filterable (filter )
10
- import Data.Foldable (any , fold )
10
+ import Data.Foldable (any , traverse_ )
11
11
import Data.String as String
12
- import Data.Traversable ( traverse_ )
12
+ import Data.String as String.CodePoint
13
13
import Effect.Aff as Aff
14
14
import Effect.Ref as Ref
15
15
import Node.FS.Sync as SyncFS
16
16
import Node.Path as Path
17
- import Record as Record
18
- import Type.Proxy (Proxy (..))
19
17
20
18
type Glob =
21
19
{ ignore :: Array String
@@ -30,8 +28,10 @@ splitGlob { ignore, include } = (\a -> { ignore, include: [ a ] }) <$> include
30
28
type Entry = { name :: String , path :: String , dirent :: DirEnt }
31
29
type FsWalkOptions = { entryFilter :: Entry -> Effect Boolean , deepFilter :: Entry -> Effect Boolean }
32
30
31
+ -- https://nodejs.org/api/fs.html#class-fsdirent
33
32
foreign import data DirEnt :: Type
34
33
foreign import isFile :: DirEnt -> Boolean
34
+
35
35
foreign import fsWalkImpl
36
36
:: (forall a b . a -> Either a b )
37
37
-> (forall a b . b -> Either a b )
@@ -40,32 +40,32 @@ foreign import fsWalkImpl
40
40
-> String
41
41
-> Effect Unit
42
42
43
- gitignoreGlob :: String -> String -> Glob
44
- gitignoreGlob base =
43
+ gitignoreFileToGlob :: FilePath -> String -> Glob
44
+ gitignoreFileToGlob base =
45
45
String .split (String.Pattern " \n " )
46
46
>>> map String .trim
47
47
>>> Array .filter (not <<< or [ String .null, isComment ])
48
48
>>> partitionMap
49
49
( \line -> do
50
- let
51
- resolve a = Path .concat [ base, a ]
52
- pat a = withForwardSlashes $ resolve $ unpackPattern a
50
+ let pattern lin = withForwardSlashes $ Path .concat [ base, gitignorePatternToGlobPattern lin ]
53
51
case String .stripPrefix (String.Pattern " !" ) line of
54
- Just negated -> Left $ pat negated
55
- Nothing -> Right $ pat line
52
+ Just negated -> Left $ pattern negated
53
+ Nothing -> Right $ pattern line
56
54
)
57
- >>> Record .rename (Proxy @" left" ) (Proxy @" ignore" )
58
- >>> Record .rename (Proxy @" right" ) (Proxy @" include" )
55
+ >>> (\{ left, right } -> { ignore: left, include: right })
59
56
60
57
where
61
58
isComment = isJust <<< String .stripPrefix (String.Pattern " #" )
62
- leadingSlash = String .stripPrefix (String.Pattern " /" )
63
- trailingSlash = String .stripSuffix (String.Pattern " /" )
59
+ dropSuffixSlash str = fromMaybe str $ String .stripSuffix (String.Pattern " /" ) str
60
+ dropPrefixSlash str = fromMaybe str $ String .stripPrefix (String.Pattern " /" ) str
61
+
62
+ leadingSlash str = String .codePointAt 0 str == Just (String.CodePoint .codePointFromChar ' /' )
63
+ trailingSlash str = String .codePointAt (String .length str - 1 ) str == Just (String.CodePoint .codePointFromChar ' /' )
64
64
65
- unpackPattern :: String -> String
66
- unpackPattern pattern
67
- | Just a <- trailingSlash pattern = unpackPattern a
68
- | Just a <- leadingSlash pattern = a <> " /**"
65
+ gitignorePatternToGlobPattern :: String -> String
66
+ gitignorePatternToGlobPattern pattern
67
+ | trailingSlash pattern = gitignorePatternToGlobPattern $ dropSuffixSlash pattern
68
+ | leadingSlash pattern = dropPrefixSlash pattern <> " /**"
69
69
| otherwise = " **/" <> pattern <> " /**"
70
70
71
71
fsWalk :: String -> Array String -> Array String -> Aff (Array Entry )
@@ -74,41 +74,63 @@ fsWalk cwd ignorePatterns includePatterns = Aff.makeAff \cb -> do
74
74
75
75
-- Pattern for directories which can be outright ignored.
76
76
-- This will be updated whenver a .gitignore is found.
77
- ignoreMatcherRef :: Ref Glob <- Ref .new { ignore: [] , include: ignorePatterns }
77
+ ignoreMatcherRef :: Ref ( String -> Boolean ) <- Ref .new (testGlob { ignore: [] , include: ignorePatterns })
78
78
79
79
-- If this Ref contains `true` because this Aff has been canceled, then deepFilter will always return false.
80
80
canceled <- Ref .new false
81
81
82
82
let
83
- entryGitignore :: Entry -> Effect Unit
84
- entryGitignore entry =
85
- try (SyncFS .readTextFile UTF8 entry.path)
86
- >>= traverse_ \gitignore ->
87
- let
88
- base = Path .relative cwd $ Path .dirname entry.path
89
- glob = gitignoreGlob base gitignore
90
- pats = splitGlob glob
91
- patOk g = not $ any (testGlob g) includePatterns
92
- newPats = filter patOk pats
93
- in
94
- void $ Ref .modify (_ <> fold newPats) $ ignoreMatcherRef
83
+ -- Update the ignoreMatcherRef with the patterns from a .gitignore file
84
+ updateIgnoreMatcherWithGitignore :: Entry -> Effect Unit
85
+ updateIgnoreMatcherWithGitignore entry = do
86
+ let
87
+ gitignorePath = entry.path
88
+ -- directory of this .gitignore relative to the directory being globbed
89
+ base = Path .relative cwd (Path .dirname gitignorePath)
90
+
91
+ try (SyncFS .readTextFile UTF8 entry.path) >>= traverse_ \gitignore -> do
92
+ let
93
+ gitignored = testGlob <$> (splitGlob $ gitignoreFileToGlob base gitignore)
94
+
95
+ -- Do not add `.gitignore` patterns that explicitly ignore the files
96
+ -- we're searching for;
97
+ --
98
+ -- ex. if `includePatterns` is [".spago/p/aff-1.0.0/**/*.purs"],
99
+ -- and `gitignored` is ["node_modules", ".spago"],
100
+ -- then add "node_modules" to `ignoreMatcher` but not ".spago"
101
+ wouldConflictWithSearch matcher = any matcher includePatterns
102
+
103
+ newMatchers = or $ filter (not <<< wouldConflictWithSearch) gitignored
104
+
105
+ -- Another possible approach could be to keep a growing array of patterns and
106
+ -- regenerate the matcher on every gitignore. We have tried that (see #1234),
107
+ -- and turned out to be 2x slower. (see #1242, and #1244)
108
+ -- Composing functions is faster, but there's the risk of blowing the stack
109
+ -- (see #1231) - when this was introduced in #1210, every match from the
110
+ -- gitignore file would be `or`ed to the previous matcher, which would create
111
+ -- a very long (linear) call chain - in this latest iteration we are `or`ing the
112
+ -- new matchers together, then the whole thing with the previous matcher.
113
+ -- This is still prone to stack issues, but we now have a tree so it should
114
+ -- not be as dramatic.
115
+ addMatcher currentMatcher = or [ currentMatcher, newMatchers ]
116
+
117
+ Ref .modify_ addMatcher ignoreMatcherRef
95
118
96
119
-- Should `fsWalk` recurse into this directory?
97
120
deepFilter :: Entry -> Effect Boolean
98
121
deepFilter entry = fromMaybe false <$> runMaybeT do
99
122
isCanceled <- lift $ Ref .read canceled
100
123
guard $ not isCanceled
101
- shouldIgnore <- lift $ testGlob <$> Ref .read ignoreMatcherRef
124
+ shouldIgnore <- lift $ Ref .read ignoreMatcherRef
102
125
pure $ not $ shouldIgnore $ Path .relative cwd entry.path
103
126
104
127
-- Should `fsWalk` retain this entry for the result array?
105
128
entryFilter :: Entry -> Effect Boolean
106
129
entryFilter entry = do
107
- when (isFile entry.dirent && entry.name == " .gitignore" ) (entryGitignore entry)
108
- ignorePat <- Ref .read ignoreMatcherRef
109
- let
110
- ignoreMatcher = testGlob ignorePat
111
- path = withForwardSlashes $ Path .relative cwd entry.path
130
+ when (isFile entry.dirent && entry.name == " .gitignore" ) do
131
+ updateIgnoreMatcherWithGitignore entry
132
+ ignoreMatcher <- Ref .read ignoreMatcherRef
133
+ let path = withForwardSlashes $ Path .relative cwd entry.path
112
134
pure $ includeMatcher path && not (ignoreMatcher path)
113
135
114
136
options = { entryFilter, deepFilter }
0 commit comments