Skip to content

Commit ff8059b

Browse files
David Brunowclaude
andcommitted
feat: enhance error handling with specific error types and recovery suggestions
- Add GitError, ParseError, and ConfigurationError types with detailed recovery suggestions - Replace generic ParserError with specific ParseError types for better diagnostics - Enhance ConventionalCommit validation to reject empty types and descriptions - Implement smart error suggestion logic that avoids redundant suggestions - Update command error handling with proper stderr output and exit codes - Fix duplicate CHANGELOG.md entries 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent ab652f3 commit ff8059b

File tree

13 files changed

+248
-240
lines changed

13 files changed

+248
-240
lines changed

CHANGELOG.md

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
* Setup CI for releases (9b70416)
1515
* Add CI (31581ad)
1616

17-
## [0.1.0] - 2024-05-20
18-
19-
### Features
20-
* Initial implementation of parsing (0c04db6)
21-
22-
### Chores
23-
* Setup CI for releases (9b70416)
24-
* Add CI (31581ad)
25-
26-
## [0.1.0] - 2024-05-20
27-
28-
### Features
29-
* Initial implementation of parsing (0c04db6)
30-
31-
### Chores
32-
* Setup CI for releases (9b70416)
33-
* Add CI (31581ad)
34-

Package.resolved

Lines changed: 13 additions & 103 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Sources/GitClient/GitClient+Live.swift

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import Model
44
extension GitClient {
55
/// No overview available.
66
public static let liveValue: GitClient = GitClient { logType in
7+
// Check if we're in a git repository
8+
guard FileManager.default.fileExists(atPath: ".git") else {
9+
throw GitError.repositoryNotFound
10+
}
711
let arguments: [String]
812
switch logType {
913
case let .branch(targetBranch):
@@ -25,29 +29,42 @@ extension GitClient {
2529
]
2630
}
2731

28-
let log = shell(
32+
let (output, exitCode) = shell(
2933
command: "git",
3034
arguments: arguments
3135
)
36+
37+
guard exitCode == 0 else {
38+
throw GitError.gitCommandFailed("git \(arguments.joined(separator: " "))", exitCode: exitCode)
39+
}
3240

33-
return log.components(separatedBy: "-@-@-@-@-@-@-@-@")
41+
return output.components(separatedBy: "-@-@-@-@-@-@-@-@")
3442
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
3543
.filter { $0.isEmpty == false }
3644
.compactMap { GitCommit($0) }
3745
} tag: {
38-
let tag = shell(
46+
// Check if we're in a git repository
47+
guard FileManager.default.fileExists(atPath: ".git") else {
48+
throw GitError.repositoryNotFound
49+
}
50+
51+
let (output, exitCode) = shell(
3952
command: "git tag --merged",
4053
arguments: []
4154
)
55+
56+
guard exitCode == 0 else {
57+
throw GitError.gitCommandFailed("git tag --merged", exitCode: exitCode)
58+
}
4259

43-
return tag.split(separator: "\n").map { String($0) }
60+
return output.split(separator: "\n").map { String($0) }
4461
}
4562
}
4663

4764
private func shell(
4865
command: String,
4966
arguments: [String]
50-
) -> String {
67+
) -> (output: String, exitCode: Int) {
5168
let script = "\(command) \(arguments.joined(separator: " "))"
5269

5370
let task = Process()
@@ -62,8 +79,11 @@ private func shell(
6279
task.standardError = errorPipe
6380

6481
try? task.run()
82+
task.waitUntilExit()
6583

6684
let data = pipe.fileHandleForReading.readDataToEndOfFile()
67-
return (String(data: data, encoding: .utf8) ?? "")
85+
let output = (String(data: data, encoding: .utf8) ?? "")
6886
.trimmingCharacters(in: .whitespacesAndNewlines)
87+
88+
return (output: output, exitCode: Int(task.terminationStatus))
6989
}

Sources/GitClient/GitClient.swift

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,36 +10,38 @@ public struct GitClient {
1010

1111
/// Returns the results of the `git log` command as an array of `GitCommit` that represent the
1212
/// commits in a git repository.
13-
/// - Parameter tag: An optional tag that, when provided, will run the `git log` command from
14-
/// HEAD to that tag.
15-
public func commitsSinceBranch(targetBranch: String) -> [GitCommit] {
16-
_log(.branch(targetBranch))
13+
/// - Parameter targetBranch: The target branch to compare against.
14+
/// - Throws: GitError if git operations fail.
15+
public func commitsSinceBranch(targetBranch: String) throws -> [GitCommit] {
16+
try _log(.branch(targetBranch))
1717
}
1818

1919
/// Returns the results of the `git log` command as an array of `GitCommit` that represent the
2020
/// commits in a git repository.
2121
/// - Parameter tag: An optional tag that, when provided, will run the `git log` command from
2222
/// HEAD to that tag.
23-
public func commitsSinceTag(_ tag: String?) -> [GitCommit] {
24-
_log(.tag(tag))
23+
/// - Throws: GitError if git operations fail.
24+
public func commitsSinceTag(_ tag: String?) throws -> [GitCommit] {
25+
try _log(.tag(tag))
2526
}
2627

2728
/// Returns the results of the `git tag` command as an array of strings that represent the tags on a
2829
/// repo.
29-
public func tag() -> [String] {
30-
_tag()
30+
/// - Throws: GitError if git operations fail.
31+
public func tag() throws -> [String] {
32+
try _tag()
3133
}
3234

33-
var _log: (LogType) -> [GitCommit] = { _ in [] }
34-
var _tag: () -> [String] = { [] }
35+
var _log: (LogType) throws -> [GitCommit] = { _ in [] }
36+
var _tag: () throws -> [String] = { [] }
3537

3638
/// Initializes a `GitClient`.
3739
/// - Parameters:
38-
/// - log: A closure that takes an optional `String` and returns an array of `GitCommit`.
40+
/// - log: A closure that takes a LogType and returns an array of `GitCommit`.
3941
/// - tag: A closure that returns an array of `String` representing git tags.
4042
public init(
41-
log: @escaping (LogType) -> [GitCommit],
42-
tag: @escaping () -> [String]
43+
log: @escaping (LogType) throws -> [GitCommit],
44+
tag: @escaping () throws -> [String]
4345
) {
4446
self._log = log
4547
self._tag = tag

Sources/Model/ConventionalCommit.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ public struct ConventionalCommit: Equatable {
9999

100100
let typeWithoutScope = type.replacingOccurrences(of: scope ?? "", with: "")
101101

102+
guard !typeWithoutScope.isEmpty else {
103+
return nil
104+
}
105+
102106
switch typeWithoutScope {
103107
case "feat":
104108
self.type = .known(.feat)
@@ -124,9 +128,15 @@ public struct ConventionalCommit: Equatable {
124128
self.isBreaking = false
125129
}
126130

127-
self.description = String(
131+
let description = String(
128132
commit.subject.suffix(from: commit.subject.index(after: colonIndex))
129133
).trimmingCharacters(in: .whitespacesAndNewlines)
134+
135+
guard !description.isEmpty else {
136+
return nil
137+
}
138+
139+
self.description = description
130140
self.hash = commit.hash
131141
self.scope = scope?
132142
.replacingOccurrences(of: "(", with: "")

Sources/Model/GitError.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import Foundation
2+
3+
/// Errors related to git operations.
4+
public enum GitError: ActionableError {
5+
case repositoryNotFound
6+
case invalidRepository
7+
case gitCommandFailed(String, exitCode: Int)
8+
9+
public var errorDescription: String? {
10+
switch self {
11+
case .repositoryNotFound:
12+
return "No git repository found in current directory"
13+
case .invalidRepository:
14+
return "Invalid git repository structure"
15+
case .gitCommandFailed(let command, let exitCode):
16+
return "Git command '\(command)' failed with exit code \(exitCode)"
17+
}
18+
}
19+
20+
public var recoverySuggestion: String? {
21+
switch self {
22+
case .repositoryNotFound:
23+
return "Ensure you're running this command from within a git repository"
24+
case .invalidRepository:
25+
return
26+
"Check that the git repository is properly initialized and not corrupted"
27+
case .gitCommandFailed(let command, _):
28+
return "Check git status and ensure '\(command)' can be run manually"
29+
}
30+
}
31+
}

Sources/Model/ParseError.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import Foundation
2+
3+
/// Errors related to parsing conventional commits and semantic versions.
4+
public enum ParseError: ActionableError {
5+
case noFormattedCommits(String)
6+
7+
public var errorDescription: String? {
8+
switch self {
9+
case .noFormattedCommits(let message):
10+
return message
11+
}
12+
}
13+
14+
public var recoverySuggestion: String? {
15+
switch self {
16+
case .noFormattedCommits:
17+
return "Ensure at least one commit follows conventional commit format"
18+
}
19+
}
20+
}

0 commit comments

Comments
 (0)