Skip to content

Commit 9d48e28

Browse files
authored
Merge pull request #9 from litso/circle-ci
Add basic CI
2 parents 7e4e9c3 + a3ab501 commit 9d48e28

File tree

7 files changed

+224
-55
lines changed

7 files changed

+224
-55
lines changed

.circleci/config.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
version: 2.1
2+
jobs:
3+
danger:
4+
macos:
5+
xcode: 13.2.1
6+
working_directory: /Users/distiller/project
7+
shell: /bin/bash --login -o pipefail
8+
steps:
9+
- checkout
10+
- run: brew install mint
11+
- run: mint bootstrap --link
12+
- run:
13+
name: Setup Environment Variables
14+
command: |
15+
echo 'export PATH=$HOME/.mint/bin:$PATH' >> $BASH_ENV
16+
- run: swiftlint version
17+
- run: brew tap danger/tap
18+
- run: brew install danger/tap/danger-swift
19+
- run:
20+
name: Danger
21+
command: danger-swift ci
22+
23+
workflows:
24+
version: 2
25+
build-test-lint:
26+
jobs:
27+
- danger
28+

.swiftlint.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
disabled_rules: # rule identifiers to exclude from running
2+
- todo
3+
- line_length
4+
- large_tuple
5+
opt_in_rules:
6+
- force_unwrapping
7+
file_length:
8+
warning: 600
9+
identifier_name:
10+
min_length: # only min_length
11+
error: 2 # only error
12+
excluded: # excluded via string array
13+
- id
14+
- f
15+
- s
16+
- i
17+
- j
18+
- x
19+
- y
20+
- vc
21+
- to
22+
function_body_length:
23+
- 60
24+
- 60
25+
nesting:
26+
type_level:
27+
warning: 2
28+
private_over_fileprivate:
29+
validate_extensions: true
30+
31+
excluded:
32+
- Example/Pods

Dangerfile.swift

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import Foundation
2+
import Danger
3+
4+
// MARK: Helper methods
5+
6+
func validateTitleAllowsMerging(for pullRequest: GitHub.PullRequest) throws {
7+
let range = NSRange(pullRequest.title.startIndex..., in: pullRequest.title)
8+
9+
let wipRegex = try NSRegularExpression(pattern: "\\bWIP\\b", options: .caseInsensitive)
10+
if wipRegex.firstMatch(in: pullRequest.title, range: range) != nil {
11+
print("Pull request title contains “WIP”.")
12+
}
13+
14+
let doNotMergeRegex = try NSRegularExpression(pattern: "\\bdo not merge\\b", options: .caseInsensitive)
15+
if doNotMergeRegex.firstMatch(in: pullRequest.title, range: range) != nil {
16+
print("Pull request title contains “do not merge”.")
17+
}
18+
}
19+
20+
func validateMessage(for ghCommit: GitHub.Commit, doesNotHavePrefixes prefixes: [String]) {
21+
let commitMessage = ghCommit.commit.message.lowercased()
22+
for prefix in prefixes where commitMessage.hasPrefix(prefix.lowercased()) {
23+
fail("Commit message \(ghCommit.sha) begins with `\(prefix)`. Squash before merging.")
24+
}
25+
}
26+
27+
func validateSwiftLintRuleChanges(for git: Git) {
28+
let hasLintConfigChanges = git.modifiedFiles.contains(".swiftlint.yml")
29+
let hasCustomRuleChanges = git.modifiedFiles.contains("SwiftLint/customrules.yml")
30+
31+
if hasLintConfigChanges, !hasCustomRuleChanges {
32+
warn("You updated the SwiftLint config. Do you also need to add a new rule to `customrules.yml`?")
33+
} else if !hasLintConfigChanges, hasCustomRuleChanges {
34+
warn("You added a new custom SwiftLint rule, don't forget to add it to DressCode's `.swiftlint.yml`!")
35+
}
36+
}
37+
38+
func messageIsGenerated(for ghCommit: GitHub.Commit) -> Bool {
39+
guard let firstWord = ghCommit.commit.message.components(separatedBy: " ").first else {
40+
return false
41+
}
42+
return firstWord == "Merge" || firstWord == "Revert" || firstWord == "fixup!" || firstWord == "squash!"
43+
}
44+
45+
/// Fails if the commit’s message does not satisfy Chris Beams’s
46+
/// [“The seven rules of a great Git commit message”](https://chris.beams.io/posts/git-commit/).
47+
func validateMessageIsGreat(for ghCommit: GitHub.Commit) {
48+
guard !messageIsGenerated(for: ghCommit) else { return }
49+
50+
let messageComponents = ghCommit.commit.message.components(separatedBy: "\n")
51+
let title = messageComponents[0]
52+
53+
// Rule 1
54+
if messageComponents.count > 1 {
55+
let separator = messageComponents[1]
56+
if !separator.isEmpty {
57+
fail("Commit message \(ghCommit.sha) is missing a blank line between title and body.")
58+
}
59+
}
60+
61+
// Rule 2
62+
if title.count > 50 {
63+
fail("Commit title \(ghCommit.sha) is \(title.count) characters long (title should be limited to 50 characters).")
64+
}
65+
66+
// Rule 3
67+
if title.first != title.capitalized.first {
68+
fail("Commit title \(ghCommit.sha) is not capitalized.")
69+
}
70+
71+
// Rule 4
72+
if title.hasSuffix(".") {
73+
fail("Commit title \(ghCommit.sha) ends with a period.")
74+
}
75+
76+
// Rule 5
77+
// Use the imperative mood in the subject line
78+
// Not validated here
79+
80+
// Rule 6
81+
let body = messageComponents.dropFirst()
82+
let lineNumbers = body.indices.map({ $0 + 1 })
83+
for (lineNumber, line) in zip(lineNumbers, body) where line.count > 72 {
84+
fail("Commit message \(ghCommit.sha) line \(lineNumber) is \(line.count) characters long (body should be wrapped at 72 characters).")
85+
}
86+
87+
// Rule 7
88+
// Use the body to explain what and why vs. how
89+
// Not validated here
90+
}
91+
92+
// MARK: - Validations
93+
94+
let danger = Danger()
95+
let pullRequest = danger.github.pullRequest
96+
97+
try validateTitleAllowsMerging(for: pullRequest)
98+
99+
validateSwiftLintRuleChanges(for: danger.git)
100+
101+
let unmergeablePrefixes = ["fixup!", "squash!", "WIP"]
102+
danger.github.commits.forEach {
103+
validateMessage(for: $0, doesNotHavePrefixes: unmergeablePrefixes)
104+
validateMessageIsGreat(for: $0)
105+
}
106+
107+
if let additions = pullRequest.additions, let deletions = pullRequest.deletions, additions < deletions {
108+
message("🎉 This PR removes more code than it adds! (\(additions - deletions) net lines)")
109+
}
110+
111+
SwiftLint.lint(inline: true, configFile: ".swiftlint.yml")

Mintfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
realm/SwiftLint@0.43.1
2+

Sources/Sections/Classes/Sections.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ extension Sections: Sequence {
3939
public typealias Iterator = AnyIterator<Section<T>>
4040

4141
public func makeIterator() -> Sections.Iterator {
42-
let g = sections.makeIterator()
43-
return AnyIterator(g)
42+
let iterator = sections.makeIterator()
43+
return AnyIterator(iterator)
4444
}
4545
}
4646

@@ -81,7 +81,7 @@ extension Sections: RangeReplaceableCollection {
8181
self.sections = []
8282
}
8383

84-
public mutating func replaceSubrange<C : Collection>(_ subRange: Range<Sections.Index>, with newElements: C) where C.Iterator.Element == Iterator.Element {
84+
public mutating func replaceSubrange<C: Collection>(_ subRange: Range<Sections.Index>, with newElements: C) where C.Iterator.Element == Iterator.Element {
8585
self.sections.replaceSubrange(subRange, with: newElements)
8686
}
8787
}

Tests/SectionsTests/EquatableTests.swift

Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -26,57 +26,54 @@ class EquatableTests: XCTestCase {
2626
Test indexPathOfValue
2727
*/
2828
func testIndexPathOfValue() {
29-
let a = EquatableType(val: "First Value")
30-
let b = EquatableType(val: "Second Value")
31-
let c = EquatableType(val: "Third Value")
29+
let rowA = EquatableType(val: "First Value")
30+
let rowB = EquatableType(val: "Second Value")
31+
let rowC = EquatableType(val: "Third Value")
3232

33-
let values = [ a, b, c]
34-
35-
let sections = SectionBuilder<EquatableType>(initialValues: []).addSections { _ in
36-
return [Section(name: "First Section", rows: [values[0]])]
33+
let sections = SectionBuilder<EquatableType>(initialValues: [])
34+
.addSections { _ in
35+
return [Section(name: "First Section", rows: [rowA])]
3736
}.addSections { _ in
38-
return [Section(name: "Second Section", rows: [values[1], values[2]])]
39-
}
37+
return [Section(name: "Second Section", rows: [rowB, rowC])]
38+
}
4039

4140
XCTAssert(sections.sections.count == 2)
4241

4342
XCTAssert(sections.sections[0].name == "First Section")
44-
XCTAssert(sections.sections[0].rows[0] == a)
43+
XCTAssert(sections.sections[0].rows[0] == rowA)
4544
XCTAssert(sections.sections[0].rows.count == 1)
4645

4746
XCTAssert(sections.sections[1].rows.count == 2)
4847
XCTAssert(sections.sections[1].name == "Second Section")
49-
XCTAssert(sections.sections[1].rows[0] == b)
50-
XCTAssert(sections.sections[1].rows[1] == c)
48+
XCTAssert(sections.sections[1].rows[0] == rowB)
49+
XCTAssert(sections.sections[1].rows[1] == rowC)
5150

52-
53-
XCTAssert(sections.indexPathOfValue(a) == IndexPath(item: 0, section: 0))
54-
XCTAssert(sections.indexPathOfValue(b) == IndexPath(item: 0, section: 1))
55-
XCTAssert(sections.indexPathOfValue(c) == IndexPath(item: 1, section: 1))
51+
XCTAssert(sections.indexPathOfValue(rowA) == IndexPath(item: 0, section: 0))
52+
XCTAssert(sections.indexPathOfValue(rowB) == IndexPath(item: 0, section: 1))
53+
XCTAssert(sections.indexPathOfValue(rowC) == IndexPath(item: 1, section: 1))
5654
}
5755

5856
func testSectionIndexable() {
5957
func testIndexPathOfValue() {
60-
let a = EquatableType(val: "First Value")
61-
let b = EquatableType(val: "Second Value")
62-
let c = EquatableType(val: "Third Value")
63-
64-
let values = [ a, b, c]
58+
let rowA = EquatableType(val: "First Value")
59+
let rowB = EquatableType(val: "Second Value")
60+
let rowC = EquatableType(val: "Third Value")
6561

66-
let sectionBuilder = SectionBuilder<EquatableType>(initialValues: []).addSections { _ in
67-
return [Section(name: "First Section", rows: [values[0]])]
62+
let sectionBuilder = SectionBuilder<EquatableType>(initialValues: [])
63+
.addSections { _ in
64+
return [Section(name: "First Section", rows: [rowA])]
6865
}.addSections { _ in
69-
return [Section(name: "Second Section", rows: [values[1], values[2]])]
70-
}
66+
return [Section(name: "Second Section", rows: [rowB, rowC])]
67+
}
7168

7269
let sections = sectionBuilder.sections
7370

7471
XCTAssert(sectionBuilder.sections.count == 2)
75-
XCTAssert(sectionBuilder.sections[0].rows[0] == a)
76-
XCTAssert(sectionBuilder.sections[1].rows[1] == c)
72+
XCTAssert(sectionBuilder.sections[0].rows[0] == rowA)
73+
XCTAssert(sectionBuilder.sections[1].rows[1] == rowC)
7774

78-
XCTAssert(sections.indexPathForRow(a) == IndexPath(item: 0, section: 0))
79-
XCTAssert(sections.indexPathForRow(a) == IndexPath(item: 1, section: 1))
75+
XCTAssert(sections.indexPathForRow(rowA) == IndexPath(item: 0, section: 0))
76+
XCTAssert(sections.indexPathForRow(rowA) == IndexPath(item: 1, section: 1))
8077
}
8178
}
8279
}

Tests/SectionsTests/Tests.swift

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,60 +11,59 @@ class SectionsTests: XCTestCase {
1111
Test creating two sections from a homogenious data type.
1212
*/
1313
func testSections() {
14-
let a = MyType(val: "First Value")
15-
let b = MyType(val: "Second Value")
14+
let rowA = MyType(val: "First Value")
15+
let rowB = MyType(val: "Second Value")
1616

17-
let sections = SectionBuilder<MyType>(initialValues: []).addSections { _ in
18-
return [Section<MyType>(name: "First", rows: [a])]
17+
let sections = SectionBuilder<MyType>(initialValues: [])
18+
.addSections { _ in
19+
return [Section<MyType>(name: "First", rows: [rowA])]
1920
}.addSections { _ in
20-
return [Section(name: "Second", rows: [b])]
21-
}
21+
return [Section(name: "Second", rows: [rowB])]
22+
}
2223

2324
XCTAssert(sections.sections.count == 2)
2425

2526
XCTAssert(sections.sections[0].name == "First")
26-
XCTAssert(sections.sections[0].rows[0] == a)
27+
XCTAssert(sections.sections[0].rows[0] == rowA)
2728
XCTAssert(sections.sections[0].rows.count == 1)
2829

2930
XCTAssert(sections.sections[1].name == "Second")
30-
XCTAssert(sections.sections[1].rows[0] == b)
31+
XCTAssert(sections.sections[1].rows[0] == rowB)
3132
XCTAssert(sections.sections[1].rows.count == 1)
3233
}
3334

3435
/**
35-
Test changing the `values` captured by the section build after the capture. Also test returning multiple sections in one closure.
36+
Test returning multiple sections in one closure.
3637
*/
3738
func testSections2() {
38-
let a = MyType(val: "First Value")
39-
let b = MyType(val: "Second Value")
40-
let c = MyType(val: "Third Value")
39+
let rowA = MyType(val: "First Value")
40+
let rowB = MyType(val: "Second Value")
41+
let rowC = MyType(val: "Third Value")
4142

42-
let values = [a, b, c]
43-
44-
let sections = SectionBuilder<MyType>(initialValues: []).addSections { _ in
45-
return [Section(name: "First", rows: [values[0]])]
43+
let sections = SectionBuilder<MyType>(initialValues: [])
44+
.addSections { _ in
45+
return [Section(name: "First", rows: [rowA])]
4646
}.addSections { _ in
47-
return [Section<MyType>(name: "Second", rows: [values[1]])]
48-
+ [Section<MyType>(name: "Third", rows: [values[2]])]
49-
}
47+
return [Section<MyType>(name: "Second", rows: [rowB])]
48+
+ [Section<MyType>(name: "Third", rows: [rowC])]
49+
}
5050

5151
XCTAssert(sections.sections.count == 3)
5252

5353
XCTAssert(sections.sections[0].name == "First")
54-
XCTAssert(sections.sections[0].rows[0] == a)
54+
XCTAssert(sections.sections[0].rows[0] == rowA)
5555
XCTAssert(sections.sections[0].rows.count == 1)
5656

5757
XCTAssert(sections.sections[1].name == "Second")
58-
XCTAssert(sections.sections[1].rows[0] == b)
58+
XCTAssert(sections.sections[1].rows[0] == rowB)
5959
XCTAssert(sections.sections[1].rows.count == 1)
6060

6161
XCTAssert(sections.sections[2].name == "Third")
62-
XCTAssert(sections.sections[2].rows[0] == c)
62+
XCTAssert(sections.sections[2].rows[0] == rowC)
6363
XCTAssert(sections.sections[2].rows.count == 1)
6464
}
6565
}
6666

67-
6867
extension SectionsTests.MyType: Equatable {}
6968

7069
func == (lhs: SectionsTests.MyType, rhs: SectionsTests.MyType) -> Bool {

0 commit comments

Comments
 (0)