forked from nsoperations/Carthage
-
Notifications
You must be signed in to change notification settings - Fork 0
/
GitIgnore.swift
188 lines (157 loc) · 5.44 KB
/
GitIgnore.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
//
// GitIgnore.swift
// CarthageKit
//
// Created by Werner Altewischer on 05/10/2019.
//
import Foundation
import wildmatch
/// Class which parses a git ignore file and validates patterns against it
final class GitIgnore {
private let parent: GitIgnore?
private var negatedPatterns = [Pattern]()
private var patterns = [Pattern]()
var copy: GitIgnore {
let copy = GitIgnore(parent: self.parent)
copy.negatedPatterns = self.negatedPatterns
copy.patterns = self.patterns
return copy
}
init(parent: GitIgnore? = nil) {
self.parent = parent
}
convenience init(string: String, parent: GitIgnore? = nil) {
self.init(parent: parent)
addPatterns(from: string)
}
convenience init(file: URL, parent: GitIgnore? = nil) throws {
self.init(parent: parent)
try addPatterns(from: file)
}
func addPatterns(from file: URL) throws {
let string = try String(contentsOf: file, encoding: .utf8)
addPatterns(from: string)
}
func addPatterns(from string: String) {
let components = string.components(separatedBy: .newlines)
for component in components {
addPattern(component)
}
}
func addPattern(_ pattern: String) {
guard let (patternString, isNegated, onlyDirectories) = GitIgnore.normalizedPattern(pattern) else {
return
}
if let pattern = Pattern(string: patternString, onlyDirectories: onlyDirectories) {
if isNegated {
negatedPatterns.append(pattern)
} else {
patterns.append(pattern)
}
}
}
func matches(relativePath: String, isDirectory: Bool) -> Bool {
let hasMatchingPattern: (Pattern) -> Bool = { $0.matches(relativePath: relativePath, isDirectory: isDirectory) }
guard !negatedPatterns.contains(where: hasMatchingPattern) else {
return false
}
guard !patterns.contains(where: hasMatchingPattern) else {
return true
}
if let parent = self.parent {
return parent.matches(relativePath: relativePath, isDirectory: isDirectory)
} else {
return false
}
}
private static func normalizedPattern(_ pattern: String) -> (pattern: String, negated: Bool, onlyDirectories: Bool)? {
guard !pattern.isEmpty else {
return nil
}
var isNegated = false
var onlyDirectories = false
var startIndex = pattern.startIndex
var endIndex = pattern.endIndex
var lastSpace = false
// Chop trailing spaces
for c in pattern.reversed() {
if c == " " {
lastSpace = true
endIndex = pattern.index(before: endIndex)
continue
} else if c == "\\" && lastSpace {
// escaped space, add it back
endIndex = pattern.index(after: endIndex)
} else if c == "/" {
onlyDirectories = true
endIndex = pattern.index(before: endIndex)
}
break
}
let firstCharacter = pattern[startIndex]
var escaped = false
if firstCharacter == "\\" {
escaped = true
// Check whether this is an escape for the next character
let nextIndex = pattern.index(after: startIndex)
if nextIndex < endIndex {
let nextCharacter = pattern[pattern.index(after: startIndex)]
switch nextCharacter {
case "!", "#":
startIndex = nextIndex
default:
break
}
}
} else if firstCharacter == "#" {
// Comment
return nil
} else if firstCharacter == "!" {
// Negated pattern
isNegated = true
startIndex = pattern.index(after: startIndex)
}
if !escaped && startIndex < endIndex && pattern[startIndex] == "/" {
// Chop leading slash
startIndex = pattern.index(after: startIndex)
}
guard startIndex < endIndex else {
return nil
}
let patternString = pattern[startIndex..<endIndex]
let normalizedString: String
if !patternString.contains("/") {
// No slash is considered to be a match in all directories
normalizedString = "**/" + patternString
} else {
normalizedString = String(patternString)
}
return (normalizedString, isNegated, onlyDirectories)
}
}
private struct Pattern {
private let rawPattern: [CChar]
private let onlyDirectories: Bool
init?(string: String, onlyDirectories: Bool) {
guard let cString = string.cString(using: .utf8) else {
return nil
}
self.rawPattern = cString
self.onlyDirectories = onlyDirectories
}
func matches(relativePath: String, isDirectory: Bool) -> Bool {
if !isDirectory && self.onlyDirectories {
return false
}
return relativePath.withCString { text -> Bool in
switch wildmatch(rawPattern, text, UInt32(WM_PATHNAME)) {
case WM_MATCH:
return true
case WM_NOMATCH:
return false
default:
return false
}
}
}
}