Skip to content

Commit cbadc47

Browse files
committed
First version: Basics work
1 parent f139f32 commit cbadc47

File tree

8 files changed

+202
-0
lines changed

8 files changed

+202
-0
lines changed

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
/*.xcodeproj

Package.resolved

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"object": {
3+
"pins": [
4+
{
5+
"package": "SwiftCLI",
6+
"repositoryURL": "https://github.com/jakeheis/SwiftCLI",
7+
"state": {
8+
"branch": null,
9+
"revision": "37f4a7f863f6fe76ce44fc0023f331eea0089beb",
10+
"version": "5.2.0"
11+
}
12+
}
13+
]
14+
},
15+
"version": 1
16+
}

Package.swift

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// swift-tools-version:4.2
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "GitAnnotateTicket",
8+
products: [
9+
.executable(name: "annotate-git-commit", targets: ["GitAnnotateTicket"]),
10+
.library(name: "GitAnnotateTicketKit", targets: ["GitAnnotateTicketKit"]),
11+
],
12+
dependencies: [
13+
.package(url: "https://github.com/jakeheis/SwiftCLI", from: "5.2.0"),
14+
],
15+
targets: [
16+
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
17+
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
18+
.target(
19+
name: "GitAnnotateTicket",
20+
dependencies: ["GitAnnotateTicketKit"]),
21+
.target(
22+
name: "GitAnnotateTicketKit",
23+
dependencies: ["SwiftCLI"]),
24+
.testTarget(
25+
name: "GitAnnotateTicketTests",
26+
dependencies: ["GitAnnotateTicketKit"]),
27+
]
28+
)

Sources/GitAnnotateTicket/main.swift

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import GitAnnotateTicketKit
2+
3+
runAnnotator()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import Foundation
2+
import SwiftCLI
3+
4+
private let ticketPrefix = "Ticket: "
5+
6+
struct AnnotatorError: Error {
7+
let message: String
8+
}
9+
10+
extension AnnotatorError: CustomStringConvertible {
11+
var description: String {
12+
return self.message
13+
}
14+
}
15+
16+
17+
func messageHasTicket(_ message: String) -> Bool {
18+
return message.split(separator: "\n").first(where: { $0.hasPrefix(ticketPrefix) }) != nil
19+
}
20+
21+
func readBranch() throws -> String {
22+
return try capture("git", arguments: ["rev-parse", "--abbrev-ref", "HEAD"]).stdout
23+
}
24+
25+
func extractFirstMatch(of regexp: NSRegularExpression, in string: String) -> String? {
26+
let fullNSRange = NSRange(location: 0, length: string.utf16.count)
27+
guard
28+
let result = regexp.firstMatch(in: string, options: [], range: fullNSRange),
29+
result.numberOfRanges == 2,
30+
let range = Range(result.range(at: 1), in: string)
31+
else {
32+
return nil
33+
}
34+
return String(string[range])
35+
}
36+
37+
func makeTicketReader(branchReader: @escaping () throws -> String) -> (NSRegularExpression) throws -> String? {
38+
return { regexp in
39+
let branch = try branchReader()
40+
guard let ticket = extractFirstMatch(of: regexp, in: branch) else {
41+
throw AnnotatorError(message: "Couldn't find ticket in branch '\(branch)' with regexp '\(regexp.pattern)'")
42+
}
43+
return ticket
44+
}
45+
}
46+
47+
func makeMessageUpdater(
48+
regexp: NSRegularExpression,
49+
ticketReader: @escaping (NSRegularExpression) throws -> String?) -> (String) throws -> String
50+
{
51+
return { message in
52+
guard !messageHasTicket(message) else { return message }
53+
guard let ticket = try ticketReader(regexp) else {
54+
throw AnnotatorError(message: "Couldn't match ticket name in branch")
55+
}
56+
57+
let messageWithTicket = message + (message.hasSuffix("\n\n") ? "" : "\n") + ticketPrefix + ticket + "\n"
58+
return messageWithTicket
59+
}
60+
}
61+
62+
func updateFile(at url: URL, with updater: (String) throws -> String) throws {
63+
let messageData = try Data(contentsOf: url)
64+
guard let message = String(bytes: messageData, encoding: .utf8) else {
65+
throw AnnotatorError(message: "Couldn't parse commit message in \(url)")
66+
}
67+
let updatedMessage = try updater(message)
68+
try Data(updatedMessage.utf8).write(to: url, options: .atomic)
69+
}
70+
71+
func parseRegexp(raw: String) throws -> NSRegularExpression {
72+
let regexp = try NSRegularExpression(pattern: raw, options: [])
73+
guard regexp.numberOfCaptureGroups == 1 else {
74+
throw AnnotatorError(message: "Regexp must have one capture group for matching the ticket name")
75+
}
76+
return regexp
77+
}
78+
79+
class AddTicket: Command {
80+
let name = "add-ticket"
81+
let rawRegexp = Parameter()
82+
let file = Parameter()
83+
84+
func execute() throws {
85+
try updateFile(
86+
at: URL(fileURLWithPath: file.value, isDirectory: false),
87+
with: makeMessageUpdater(
88+
regexp: try parseRegexp(raw: rawRegexp.value),
89+
ticketReader: makeTicketReader(branchReader: readBranch)))
90+
}
91+
}
92+
93+
public func runAnnotator() {
94+
let argv = ProcessInfo.processInfo.arguments
95+
let annotator = CLI(name: argv[0])
96+
annotator.commands = [AddTicket()]
97+
exit(annotator.go())
98+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//
2+
// MessageUpdaterTests.swift
3+
// git-branch-commitTests
4+
//
5+
// Created by Juri Pakaste on 14/11/2018.
6+
//
7+
8+
import XCTest
9+
@testable import GitAnnotateTicketKit
10+
11+
let regexp = try! NSRegularExpression(pattern: "\\b(ch\\d+)\\b", options: [])
12+
let matchingBranch = "feature/ch1234/foo"
13+
let nonMatchingBranch = "feature/no-ticket-here"
14+
15+
class MessageUpdaterTests: XCTestCase {
16+
func testUpdateEmpty() throws {
17+
let messageUpdater = makeMessageUpdater(regexp: regexp, ticketReader: makeTicketReader(branchReader: { matchingBranch }))
18+
XCTAssertEqual(try messageUpdater(""), "\nTicket: ch1234\n")
19+
}
20+
21+
func testUpdateNonEmpty() throws {
22+
let messageUpdater = makeMessageUpdater(regexp: regexp, ticketReader: makeTicketReader(branchReader: { matchingBranch }))
23+
XCTAssertEqual(try messageUpdater("lines\nof\ntext\n"), "lines\nof\ntext\n\nTicket: ch1234\n")
24+
}
25+
26+
func testUpdateNonEmptyNoExtraLines() throws {
27+
let messageUpdater = makeMessageUpdater(regexp: regexp, ticketReader: makeTicketReader(branchReader: { matchingBranch }))
28+
XCTAssertEqual(try messageUpdater("lines\nof\ntext\n\n"), "lines\nof\ntext\n\nTicket: ch1234\n")
29+
}
30+
31+
static var allTests = [
32+
("testUpdateEmpty", testUpdateEmpty),
33+
("testUpdateNonEmpty", testUpdateNonEmpty),
34+
("testUpdateNonEmptyNoExtraLines", testUpdateNonEmptyNoExtraLines),
35+
]
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import XCTest
2+
3+
#if !os(macOS)
4+
public func allTests() -> [XCTestCaseEntry] {
5+
return [
6+
testCase(GitAnnotateTicketTests.allTests),
7+
testCase(MessageUpdaterTests.allTests)
8+
]
9+
}
10+
#endif

Tests/LinuxMain.swift

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import XCTest
2+
3+
import GitAnnotateTicketTests
4+
5+
var tests = [XCTestCaseEntry]()
6+
tests += GitAnnotateTicketTests.allTests()
7+
XCTMain(tests)

0 commit comments

Comments
 (0)