Skip to content

Commit

Permalink
adds implementation of pathspec with gitignore style matching
Browse files Browse the repository at this point in the history
  • Loading branch information
g-Off committed Jun 30, 2019
1 parent 4f6a5b7 commit 7089699
Show file tree
Hide file tree
Showing 8 changed files with 597 additions and 1 deletion.
78 changes: 78 additions & 0 deletions .swiftpm/xcode/xcshareddata/xcschemes/Pathspec.xcscheme
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1100"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Pathspec"
BuildableName = "Pathspec"
BlueprintName = "Pathspec"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "PathspecTests"
BuildableName = "PathspecTests"
BlueprintName = "PathspecTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Pathspec"
BuildableName = "Pathspec"
BlueprintName = "Pathspec"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
21 changes: 21 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// swift-tools-version:5.0
import PackageDescription

let package = Package(
name: "Pathspec",
products: [
.library(
name: "Pathspec",
targets: ["Pathspec"]),
],
dependencies: [
],
targets: [
.target(
name: "Pathspec",
dependencies: []),
.testTarget(
name: "PathspecTests",
dependencies: ["Pathspec"]),
]
)
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
# Pathspec
# Pathspec

A Swift library for pattern matching file paths using git's matching format (used for [.gitignore](http://git-scm.com/docs/gitignore) files)

Ported from:
- [Python pathspec](https://github.com/cpburnz/python-path-specification)
- [Ruby pathspec](https://github.com/highb/pathspec-ruby)
154 changes: 154 additions & 0 deletions Sources/Pathspec/GitIgnoreSpec.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
//
// GitIgnoreSpec.swift
// Pathspec
//
// Created by Geoffrey Foster on 2019-06-29.
//

import Foundation

class GitIgnoreSpec: Spec {
private(set) var inclusive: Bool = true
let regex: NSRegularExpression

init?(pattern: String) {
guard !pattern.isEmpty else { return nil }
guard !pattern.hasPrefix("#") else { return nil }
guard !pattern.contains("***") else { return nil }
guard pattern != "/" else { return nil }

var pattern = pattern
if pattern.hasPrefix("!") {
inclusive = false
pattern.removeFirst()
}

if pattern.starts(with: "\\") {
pattern.removeFirst(2)
}

var patternSegments = pattern.components(separatedBy: "/")
if patternSegments[0].isEmpty {
patternSegments.removeFirst()
} else if patternSegments.count == 1 || (patternSegments.count == 2 && patternSegments[1].isEmpty) {
if patternSegments[0] != "**" {
patternSegments.insert("**", at: 0)
}
}
if patternSegments.count > 1 && patternSegments[patternSegments.index(before: patternSegments.endIndex)].isEmpty {
patternSegments[patternSegments.index(before: patternSegments.endIndex)] = "**"
}

let pathSeparator = "/"
var regexString = "^"
var needSlash = false
let lastIndex = patternSegments.index(before: patternSegments.endIndex)
for index in patternSegments.startIndex..<patternSegments.endIndex {
let segment = patternSegments[index]

if segment == "**" {
if index == patternSegments.startIndex && index == lastIndex {
regexString += ".+"
} else if index == patternSegments.startIndex {
regexString += "(?:.+\(pathSeparator))?"
needSlash = false
} else if index == lastIndex {
regexString += "\(pathSeparator).*"
} else {
regexString += "(?:\(pathSeparator).+)?"
needSlash = true
}
} else if segment == "*" {
if needSlash {
regexString += "/"
}
regexString += "[^\(pathSeparator)+"
needSlash = true
} else {
if needSlash {
regexString += "/"
}

regexString += GitIgnoreSpec.globToRegularExpression(glob: segment)

if inclusive && index == lastIndex {
regexString += "(?:/.*)?"
}

needSlash = true
}
}

regexString += "$"

do {
regex = try NSRegularExpression(pattern: regexString, options: [])
} catch {
return nil
}
}

func match(file: String) -> Bool {
return regex.firstMatch(in: file, options: [], range: NSRange(file.startIndex..<file.endIndex, in: file)) != nil
}

private static func globToRegularExpression(glob: String) -> String {
var regex = ""
var escape = false
var i = glob.startIndex
while i < glob.endIndex {
let char = glob[i]
i = glob.index(after: i)

if escape {
escape = false
regex += NSRegularExpression.escapedPattern(for: "\(char)")
} else if char == "\\" {
escape = true
} else if char == "*" {
regex += "[^/]*"
} else if char == "?" {
regex += "[^/]"
} else if char == "[" {
var j = i
if j < glob.endIndex && glob[j] == "!" {
j = glob.index(after: j)
}
if j < glob.endIndex && glob[j] == "]" {
j = glob.index(after: j)
}
while j < glob.endIndex && glob[j] != "]" {
j = glob.index(after: j)
}
if j < glob.endIndex {
var expr = "["

if glob[i] == "!" {
expr += "^"
i = glob.index(after: i)
} else if glob[i] == "^" {
expr += #"\^"#
i = glob.index(after: i)
}

if glob[i] == "]" && i != j {
expr += #"\]"#
i = glob.index(after: i)
}

expr += glob[i...j].replacingOccurrences(of: "\\", with: "\\\\")
regex += expr

j = glob.index(after: j)
i = j
} else {
regex += #"\["#
}
} else {
regex += NSRegularExpression.escapedPattern(for: "\(char)")
}
}

return regex
}
}
41 changes: 41 additions & 0 deletions Sources/Pathspec/Pathspec.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// Pathspec.swift
// Pathspec
//
// Created by Geoffrey Foster on 2019-06-29.
//

public final class Pathspec {
public enum Kind {
case git
case regex
}
private var specs: [Spec] = []

public init(kind: Kind, patterns: String...) {
for pattern in patterns {
add(pattern: pattern, kind: kind)
}
}

public func match(path: String) -> Bool {
return matchingSpecs(path: path).allSatisfy { $0.inclusive }
}

func add(pattern: String, kind: Kind) {
let spec: Spec?
switch kind {
case .git:
spec = GitIgnoreSpec(pattern: pattern)
case .regex:
spec = RegexSpec()
}
if let spec = spec {
specs.append(spec)
}
}

private func matchingSpecs(path: String) -> [Spec] {
return specs.filter { $0.match(file: path) }
}
}
16 changes: 16 additions & 0 deletions Sources/Pathspec/RegexSpec.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// RegexSpec.swift
// Pathspec
//
// Created by Geoffrey Foster on 2019-06-29.
//

import Foundation

class RegexSpec: Spec {
var inclusive: Bool = false

func match(file: String) -> Bool {
return false
}
}
21 changes: 21 additions & 0 deletions Sources/Pathspec/Spec.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// Spec.swift
// Pathspec
//
// Created by Geoffrey Foster on 2019-06-29.
//

import Foundation

public protocol Spec {
var inclusive: Bool { get }
func match(file: String) -> Bool
}

extension Spec {
public func match(files: [String]) -> [String] {
return files.filter { (file) -> Bool in
match(file: file)
}
}
}
Loading

0 comments on commit 7089699

Please sign in to comment.