Skip to content

Commit 1c6d221

Browse files
Wei SunWei Sun
authored andcommitted
[Implementation] Add implementation for commit lint
- lint itself and its configuraiton
1 parent c944159 commit 1c6d221

16 files changed

+330
-63
lines changed

Sources/DangerSwiftCommitLint/CommitChecker/BodyEmptyLineCheck.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import Foundation
22

3-
struct BodyEmptyLineCheck: CommitChecker {
4-
static var warningMessage = "Please separate commit message subject from body with newline."
3+
struct BodyEmptyLineCheck: CommitChecker, Hashable {
4+
static var checkerMessage = "Please separate commit message subject from body with newline."
55

66
private let bodyLinesOfText: [String]
77

8-
init(message: CommitMessage) {
9-
bodyLinesOfText = message.bodyLinesOfText
8+
init(_ commitMessage: CommitMessage) {
9+
bodyLinesOfText = commitMessage.bodyLinesOfText
1010
}
1111

1212
var fail: Bool {
Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import Foundation
22

3-
protocol CommitChecker: Hashable {
4-
static var warningMessage: String { get }
3+
public protocol CommitChecker {
4+
static var checkerMessage: String { get }
5+
56
var fail: Bool { get }
67

7-
init(message: CommitMessage)
8+
init(_ commitMessage: CommitMessage)
89

9-
static func fail(message: CommitMessage) -> Bool
10+
static func fail(_ commitMessage: CommitMessage) -> Bool
1011
}
1112

12-
extension CommitChecker {
13-
static func fail(message: CommitMessage) -> Bool {
14-
Self(message: message).fail
13+
public extension CommitChecker {
14+
static func fail(_ commitMessage: CommitMessage) -> Bool {
15+
Self(commitMessage).fail
1516
}
1617
}
Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import Danger
22
import Foundation
33

4-
struct CommitMessage: Hashable {
4+
/// An abstraction of GitHub commit message.
5+
public struct CommitMessage: Hashable {
56
/// First line of the commit message
6-
let subject: String
7+
public let subject: String
78
/// Rest of the commit message
8-
let bodyLinesOfText: [String]
9+
public let bodyLinesOfText: [String]
910
// Commit SHA value
10-
let sha: String
11+
public let sha: String?
1112

1213
init(
1314
subject: String,
@@ -19,10 +20,21 @@ struct CommitMessage: Hashable {
1920
self.sha = sha
2021
}
2122

22-
init(_ commit: GitHub.Commit) {
23+
/// Initialize CommitMessage with `Danger.GitHub.Commit.CommitData.message`.
24+
/// - Parameter commit: An instance of `GitHub.Commit`
25+
public init(_ commit: GitHub.Commit) {
2326
let commitMessageLines = commit.commit.message.components(separatedBy: .newlines)
2427
subject = commitMessageLines.first ?? ""
2528
bodyLinesOfText = Array(commitMessageLines.dropFirst())
2629
sha = commit.sha
2730
}
31+
32+
/// Initialize `CommitMessage` with `Danger.Git.Commit.message`
33+
/// - Parameter gitCommit: An instance of `Danger.Git.Commit`
34+
public init(_ gitCommit: Git.Commit) {
35+
let commitMessageLines = gitCommit.message.components(separatedBy: .newlines)
36+
subject = commitMessageLines.first ?? ""
37+
bodyLinesOfText = Array(commitMessageLines.dropFirst())
38+
sha = gitCommit.sha
39+
}
2840
}

Sources/DangerSwiftCommitLint/CommitChecker/SubjectCapCheck.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import Foundation
22

3-
struct SubjectCapCheck: CommitChecker {
4-
static let warningMessage = "Please start commit message subject with capital letter."
3+
struct SubjectCapCheck: CommitChecker, Hashable {
4+
static let checkerMessage = "Please start commit message subject with capital letter."
55

66
private let firstCharacter: Character?
77

8-
init(message: CommitMessage) {
9-
firstCharacter = message.subject.first
8+
init(_ commitMessage: CommitMessage) {
9+
firstCharacter = commitMessage.subject.first
1010
}
1111

1212
var fail: Bool {

Sources/DangerSwiftCommitLint/CommitChecker/SubjectLengthCheck.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import Foundation
22

3-
struct SubjectLengthCheck: CommitChecker {
3+
struct SubjectLengthCheck: CommitChecker, Hashable {
44
private enum GeneratedSubjectPattern {
55
static let git = #"^Merge branch '.+' into "#
66
static let gitHub = #"^Merge pull request #\d+ from "#
77
}
88

9-
static let warningMessage = "Please limit commit message subject line to 50 characters."
9+
static let checkerMessage = "Please limit commit message subject line to 50 characters."
1010

1111
private let subject: String
1212

13-
init(message: CommitMessage) {
14-
subject = message.subject
13+
init(_ commitMessage: CommitMessage) {
14+
subject = commitMessage.subject
1515
}
1616

1717
var fail: Bool {

Sources/DangerSwiftCommitLint/CommitChecker/SubjectPeriodCheck.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import Foundation
22

3-
struct SubjectPeriodCheck: CommitChecker {
4-
static var warningMessage = "Please remove period from end of commit message subject line."
3+
struct SubjectPeriodCheck: CommitChecker, Hashable {
4+
static var checkerMessage = "Please remove period from end of commit message subject line."
55

66
private let subject: String
77

8-
init(message: CommitMessage) {
9-
subject = message.subject
8+
init(_ commitMessage: CommitMessage) {
9+
subject = commitMessage.subject
1010
}
1111

1212
var fail: Bool {

Sources/DangerSwiftCommitLint/CommitChecker/SubjectWordCheck.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import Foundation
22

3-
struct SubjectWordCheck: CommitChecker {
4-
static var warningMessage = "Please use more than one word in commit message."
3+
struct SubjectWordCheck: CommitChecker, Hashable {
4+
static var checkerMessage = "Please use more than one word in commit message."
55

66
private let subject: String
77

8-
init(message: CommitMessage) {
9-
subject = message.subject
8+
init(_ commitMessage: CommitMessage) {
9+
subject = commitMessage.subject
1010
}
1111

1212
var fail: Bool {
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import Foundation
2+
3+
public extension DangerSwiftCommitLint {
4+
struct Configuration {
5+
/// All commit checkers provided by `DangerSwiftCommitLint`
6+
public enum CommitCheckerType: CaseIterable, Hashable {
7+
/// Commit subject and body are separated by an empty line (`CommitChecker/BodyEmptyLineCheck`)
8+
case bodyEmptyLine
9+
/// Commit subject begins with a capital letter (`CommitChecker/SubjectCapCheck`)
10+
case subjectCapCheck
11+
/// Commit subject is no longer than 50 characters (`CommitChecker/SubjectLengthCheck`)
12+
case subjectLengthCheck
13+
/// Commit subject does not end in a period (`CommitChecker/SubjectPeriodCheck`)
14+
case subjectPeriodCheck
15+
/// Commit subject is more than one word (`CommitChecker/SubjectWordCheck`)
16+
case subjectWordCheck
17+
}
18+
19+
/// Checker selection
20+
public enum CommitCheckerSelection {
21+
/// Select all checkers
22+
case all
23+
/// Select a set of checkers
24+
case selected(Set<CommitCheckerType>)
25+
}
26+
27+
private let disabled: CommitCheckerSelection
28+
private let warn: CommitCheckerSelection
29+
private let fail: CommitCheckerSelection
30+
let limit: Int
31+
private let customCheckers: [CommitChecker.Type]
32+
33+
/// Initialize the configuraiton.
34+
/// - Parameters:
35+
/// - disabled: The selected checks to skip.
36+
/// - warn: The selected checks to warn on.
37+
/// - fail: The selected checks to fail on.
38+
/// - limit: The number of commits to check.
39+
/// - customCheckers: An array of custom checkers.
40+
public init(
41+
disabled: CommitCheckerSelection = .selected([]),
42+
warn: CommitCheckerSelection = .selected([]),
43+
fail: CommitCheckerSelection = .all,
44+
limit: Int = 0,
45+
customCheckers: [CommitChecker.Type] = []
46+
) {
47+
self.disabled = disabled
48+
self.warn = warn
49+
self.fail = fail
50+
self.limit = limit
51+
self.customCheckers = customCheckers
52+
}
53+
}
54+
}
55+
56+
extension DangerSwiftCommitLint.Configuration {
57+
static var defaultCheckers: [CommitChecker.Type] {
58+
CommitCheckerType.allCases.map(\.type)
59+
}
60+
61+
var allCheckers: [CommitChecker.Type] {
62+
Self.defaultCheckers + customCheckers
63+
}
64+
65+
var disabledCheckers: [CommitChecker.Type] {
66+
switch disabled {
67+
case .all:
68+
return allCheckers
69+
case let .selected(disabled):
70+
return disabled.map(\.type)
71+
}
72+
}
73+
74+
var enabledCheckers: [CommitChecker.Type] {
75+
allCheckers.filter { checker in
76+
disabledCheckers.contains { $0 == checker } == false
77+
}
78+
}
79+
80+
var warningCheckers: [CommitChecker.Type] {
81+
switch warn {
82+
case .all:
83+
return allCheckers
84+
case let .selected(warningCheckers):
85+
return enabledCheckers.filter { type in
86+
warningCheckers.map(\.type).contains { $0 == type }
87+
}
88+
}
89+
}
90+
91+
var failingCheckers: [CommitChecker.Type] {
92+
enabledCheckers.filter { type in
93+
warningCheckers.contains { $0 == type } == false
94+
}
95+
}
96+
}
97+
98+
private extension DangerSwiftCommitLint.Configuration.CommitCheckerType {
99+
var type: CommitChecker.Type {
100+
switch self {
101+
case .bodyEmptyLine: return BodyEmptyLineCheck.self
102+
case .subjectCapCheck: return SubjectCapCheck.self
103+
case .subjectLengthCheck: return SubjectCapCheck.self
104+
case .subjectPeriodCheck: return SubjectPeriodCheck.self
105+
case .subjectWordCheck: return SubjectWordCheck.self
106+
}
107+
}
108+
}
Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,69 @@
11
import Danger
22
import Foundation
33

4-
public final class DangerSwiftCommitLint {}
4+
public final class DangerSwiftCommitLint {
5+
private enum Constants {
6+
static let disableAllChecksMessage = "All checks were disabled, nothing to do."
7+
}
8+
9+
private let danger: DangerDSL
10+
private var configuration: Configuration!
11+
12+
/// Initialize an instance of the linter
13+
/// - Parameters:
14+
/// - danger: An instance of `Danger.DangerDSL`.
15+
/// - configuration: Linter configuration.
16+
init(danger: DangerDSL = Danger(), configuration: Configuration) {
17+
self.danger = danger
18+
self.configuration = configuration
19+
}
20+
21+
/// Lints the PR commits.
22+
public func check() {
23+
if configuration.enabledCheckers.isEmpty {
24+
danger.warn(Constants.disableAllChecksMessage)
25+
} else {
26+
checkMessages()
27+
}
28+
}
29+
}
30+
31+
extension DangerSwiftCommitLint {
32+
var commitMessages: [CommitMessage] {
33+
let messages = danger.git.commits.map { CommitMessage($0) }
34+
guard configuration.limit > 0 else {
35+
return messages
36+
}
37+
38+
return Array(messages.suffix(configuration.limit))
39+
}
40+
41+
func checkMessages() {
42+
// Checkers that warn the danger job.
43+
configuration.warningCheckers.checkCommitMessages(commitMessages, checkerResultHanler: warning(_:shas:))
44+
45+
// Checkers that fail the danger job.
46+
configuration.failingCheckers.checkCommitMessages(commitMessages, checkerResultHanler: failing(_:shas:))
47+
}
48+
49+
func warning(_ message: String, shas: [String]) {
50+
let warningMessage = ([message] + shas).joined(separator: "\n")
51+
danger.warn(warningMessage)
52+
}
53+
54+
func failing(_ message: String, shas: [String]) {
55+
let failingMessage = ([message] + shas).joined(separator: "\n")
56+
danger.warn(failingMessage)
57+
}
58+
}
59+
60+
extension Array where Element == CommitChecker.Type {
61+
func checkCommitMessages(_ commitMessages: [CommitMessage], checkerResultHanler: (String, [String]) -> Void) {
62+
forEach { checker in
63+
let shas = commitMessages.compactMap { checker.fail($0) ? $0.sha : nil }
64+
if shas.isEmpty == false {
65+
checkerResultHanler(checker.checkerMessage, shas)
66+
}
67+
}
68+
}
69+
}

Tests/DangerSwiftCommitLintTests/CommitChecker/BodyEmptyLineCheckTests.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,20 @@ final class BodyEmptyLineCheckTests: XCTestCase {
88
private let commitNoNewline = CommitMessage(subject: "Title Test", bodyLinesOfText: ["Body Test"], sha: "Test SHA")
99

1010
func testSuccessCommitSubjectAndBody() {
11-
let testSubject = BodyEmptyLineCheck(message: commitSubjectAndBody)
11+
let testSubject = BodyEmptyLineCheck(commitSubjectAndBody)
1212
XCTAssertFalse(testSubject.fail)
13-
XCTAssertFalse(BodyEmptyLineCheck.fail(message: commitSubjectAndBody))
13+
XCTAssertFalse(BodyEmptyLineCheck.fail(commitSubjectAndBody))
1414
}
1515

1616
func testSuccessSubjectOnly() {
17-
let testSubject = BodyEmptyLineCheck(message: commitSubjectOnly)
17+
let testSubject = BodyEmptyLineCheck(commitSubjectOnly)
1818
XCTAssertFalse(testSubject.fail)
19-
XCTAssertFalse(BodyEmptyLineCheck.fail(message: commitSubjectOnly))
19+
XCTAssertFalse(BodyEmptyLineCheck.fail(commitSubjectOnly))
2020
}
2121

2222
func testFailureNoNewlineOnly() {
23-
let testSubject = BodyEmptyLineCheck(message: commitNoNewline)
23+
let testSubject = BodyEmptyLineCheck(commitNoNewline)
2424
XCTAssertTrue(testSubject.fail)
25-
XCTAssertTrue(BodyEmptyLineCheck.fail(message: commitNoNewline))
25+
XCTAssertTrue(BodyEmptyLineCheck.fail(commitNoNewline))
2626
}
2727
}

0 commit comments

Comments
 (0)