diff --git a/.spi.yml b/.spi.yml index a0bc982..1d6c3ad 100644 --- a/.spi.yml +++ b/.spi.yml @@ -1,4 +1,4 @@ version: 1 builder: configs: - documentation_targets: [CliVersion] + - documentation_targets: [CliVersion] diff --git a/Sources/CliVersion/CliClient.swift b/Sources/CliVersion/CliClient.swift index 895c88a..3a014d2 100644 --- a/Sources/CliVersion/CliClient.swift +++ b/Sources/CliVersion/CliClient.swift @@ -18,6 +18,8 @@ public extension DependencyValues { @DependencyClient public struct CliClient: Sendable { + static let defaultFileName = "Version.swift" + /// Build and update the version based on the git tag, or branch + sha. public var build: @Sendable (SharedOptions) async throws -> String @@ -36,22 +38,22 @@ public struct CliClient: Sendable { public struct SharedOptions: Equatable, Sendable { + let allowPreReleaseTag: Bool let dryRun: Bool - let fileName: String let gitDirectory: String? let logLevel: Logger.Level let target: String public init( + allowPreReleaseTag: Bool = false, gitDirectory: String? = nil, dryRun: Bool = false, - fileName: String = "Version.swift", target: String, logLevel: Logger.Level = .debug ) { + self.allowPreReleaseTag = allowPreReleaseTag self.gitDirectory = gitDirectory self.dryRun = dryRun - self.fileName = fileName self.target = target self.logLevel = logLevel } @@ -81,18 +83,27 @@ extension CliClient: DependencyKey { @_spi(Internal) public extension CliClient.SharedOptions { - var fileUrl: URL { + func fileUrl() async throws -> URL { + @Dependency(\.fileClient) var fileClient + let target = self.target.hasPrefix(".") ? String(self.target.dropFirst()) : self.target let targetHasSources = target.hasPrefix("Sources") || target.hasPrefix("/Sources") var url = url(for: gitDirectory ?? (targetHasSources ? target : "Sources")) + if gitDirectory != nil { if !targetHasSources { url.appendPathComponent("Sources") } url.appendPathComponent(target) } - url.appendPathComponent(fileName) + + let isDirectory = try await fileClient.isDirectory(url.cleanFilePath) + + if isDirectory { + url.appendPathComponent(CliClient.defaultFileName) + } + return url } @@ -121,24 +132,62 @@ public extension CliClient.SharedOptions { private extension CliClient.SharedOptions { + func gitVersion() async throws -> GitClient.Version { + @Dependency(\.gitClient) var gitClient + + if let exactMatch = try? await gitClient.version(.init( + gitDirectory: gitDirectory, + style: .tag(exactMatch: true) + )) { + return exactMatch + } else if let partialMatch = try? await gitClient.version(.init( + gitDirectory: gitDirectory, + style: .tag(exactMatch: false) + )) { + return partialMatch + } else { + return try await gitClient.version(.init( + gitDirectory: gitDirectory, + style: .branch(commitSha: true) + )) + } + } + + func gitSemVar() async throws -> SemVar { + @Dependency(\.gitClient) var gitClient + + let version = try await gitVersion() + + guard let semVar = version.semVar else { + return .init(preRelease: version.description) + } + + if allowPreReleaseTag, semVar.preRelease == nil { + let branchVersion = try await gitClient.version(.init( + gitDirectory: gitDirectory, + style: .branch(commitSha: true) + )) + return .init( + major: semVar.major, + minor: semVar.minor, + patch: semVar.patch, + preRelease: branchVersion.description + ) + } + return semVar + } + func build(_ environment: [String: String]) async throws -> String { try await run { - @Dependency(\.gitVersionClient) var gitVersion + @Dependency(\.gitClient) var gitVersion @Dependency(\.fileClient) var fileClient @Dependency(\.logger) var logger - let gitDirectory = gitDirectory ?? environment["PWD"] - - guard let gitDirectory else { - throw CliClientError.gitDirectoryNotFound - } - - logger.debug("Building with git directory: \(gitDirectory)") - - let fileUrl = self.fileUrl + let fileUrl = try await self.fileUrl() logger.debug("File url: \(fileUrl.cleanFilePath)") let currentVersion = try await gitVersion.currentVersion(in: gitDirectory) + logger.debug("Git version: \(currentVersion)") let fileContents = Template.build(currentVersion) @@ -150,9 +199,10 @@ private extension CliClient.SharedOptions { private func getVersionString() async throws -> (version: String, usesOptionalType: Bool) { @Dependency(\.fileClient) var fileClient - @Dependency(\.gitVersionClient) var gitVersionClient + @Dependency(\.gitClient) var gitVersionClient @Dependency(\.logger) var logger - let targetUrl = fileUrl + + let targetUrl = try await fileUrl() guard fileClient.fileExists(targetUrl) else { // Get the latest tag, not requiring an exact tag set on the commit. @@ -163,7 +213,7 @@ private extension CliClient.SharedOptions { return (version, false) } - let contents = try await fileClient.read(fileUrl) + let contents = try await fileClient.read(targetUrl) let versionLine = contents.split(separator: "\n") .first { $0.hasPrefix("let VERSION:") } @@ -182,41 +232,23 @@ private extension CliClient.SharedOptions { return (String(versionString), isOptional) } - // swiftlint:disable large_tuple - private func getVersionParts(_ version: String, _ bump: CliClient.BumpOption) throws -> (Int, Int, Int) { - @Dependency(\.logger) var logger - let parts = String(version).split(separator: ".") - logger.debug("Version parts: \(parts)") - - // TODO: Better error. - guard parts.count == 3 else { - throw CliClientError.failedToParseVersionFile - } - - var major = Int(String(parts[0].replacingOccurrences(of: "\"", with: ""))) ?? 0 - var minor = Int(String(parts[1])) ?? 0 - - // Handle cases where patch version has extra data / not an exact match tag. - // Such as: 0.1.1-4-g59bc977 - var patch = Int(String(parts[2].split(separator: "-").first ?? "0")) ?? 0 - bump.bump(major: &major, minor: &minor, patch: &patch) - return (major, minor, patch) + private func getSemVar(_ version: String, _ bump: CliClient.BumpOption) throws -> SemVar { + let semVar = SemVar(string: version) ?? .init() + return semVar.bump(bump) } - // swiftlint:enable large_tuple - func bump(_ type: CliClient.BumpOption) async throws -> String { try await run { @Dependency(\.fileClient) var fileClient @Dependency(\.logger) var logger - let targetUrl = fileUrl + let targetUrl = try await fileUrl() logger.debug("Bump target url: \(targetUrl.cleanFilePath)") let (versionString, usesOptional) = try await getVersionString() - let (major, minor, patch) = try getVersionParts(versionString, type) - let version = "\(major).\(minor).\(patch)" + let semVar = try getSemVar(versionString, type) + let version = semVar.versionString(allowPrerelease: allowPreReleaseTag) logger.debug("Bumped version: \(version)") let template = usesOptional ? Template.optional(version) : Template.build(version) @@ -230,7 +262,7 @@ private extension CliClient.SharedOptions { @Dependency(\.fileClient) var fileClient @Dependency(\.logger) var logger - let targetUrl = fileUrl + let targetUrl = try await fileUrl() logger.debug("Generate target url: \(targetUrl.cleanFilePath)") @@ -245,7 +277,7 @@ private extension CliClient.SharedOptions { } func update() async throws -> String { - @Dependency(\.gitVersionClient) var gitVersionClient + @Dependency(\.gitClient) var gitVersionClient return try await generate(gitVersionClient.currentVersion(in: gitDirectory)) } } diff --git a/Sources/CliVersion/FileClient.swift b/Sources/CliVersion/FileClient.swift index 2e061bb..5542656 100644 --- a/Sources/CliVersion/FileClient.swift +++ b/Sources/CliVersion/FileClient.swift @@ -8,6 +8,7 @@ import XCTestDynamicOverlay // TODO: This can be an internal dependency. +@_spi(Internal) public extension DependencyValues { /// Access a basic ``FileClient`` that can read / write data to the file system. @@ -26,12 +27,19 @@ public extension DependencyValues { /// @Dependency(\.fileClient) var fileClient /// ``` /// +@_spi(Internal) @DependencyClient public struct FileClient: Sendable { + /// Return the current working directory. + public var currentDirectory: @Sendable () async throws -> String + /// Check if a file exists at the given url. public var fileExists: @Sendable (URL) -> Bool = { _ in true } + /// Check if a url is a directory. + public var isDirectory: @Sendable (String) async throws -> Bool + /// Read the contents of a file. public var read: @Sendable (URL) async throws -> String @@ -65,11 +73,14 @@ public struct FileClient: Sendable { } } +@_spi(Internal) extension FileClient: DependencyKey { /// A ``FileClient`` that does not do anything. public static let noop = FileClient( + currentDirectory: { "./" }, fileExists: { _ in true }, + isDirectory: { _ in true }, read: { _ in "" }, write: { _, _ in } ) @@ -79,17 +90,24 @@ extension FileClient: DependencyKey { /// The live ``FileClient`` public static let liveValue = FileClient( + currentDirectory: { FileManager.default.currentDirectoryPath }, fileExists: { FileManager.default.fileExists(atPath: $0.cleanFilePath) }, + isDirectory: { path in + var isDirectory: ObjCBool = false + FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) + return isDirectory.boolValue + }, read: { try String(contentsOf: $0, encoding: .utf8) }, write: { try $0.write(to: $1, options: .atomic) } ) - @_spi(Internal) public static func capturing( _ captured: CapturingWrite ) -> Self { .init( + currentDirectory: { "./" }, fileExists: { _ in true }, + isDirectory: { _ in true }, read: { _ in "" }, write: { await captured.set($0, $1) } ) diff --git a/Sources/CliVersion/GitClient.swift b/Sources/CliVersion/GitClient.swift new file mode 100644 index 0000000..b12ad9d --- /dev/null +++ b/Sources/CliVersion/GitClient.swift @@ -0,0 +1,265 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif +import Dependencies +import DependenciesMacros +import ShellClient + +@_spi(Internal) +public extension DependencyValues { + + /// A ``GitVersionClient`` that can retrieve the current version from a + /// git directory. + var gitClient: GitClient { + get { self[GitClient.self] } + set { self[GitClient.self] = newValue } + } +} + +/// A client that can retrieve the current version from a git directory. +/// It will use the current `tag`, or if the current git tree does not +/// point to a commit that is tagged, it will use the `branch git-sha` as +/// the version. +/// +/// This is often not used directly, instead it is used with one of the plugins +/// that is supplied with this library. The use case is to set the version of a command line +/// tool based on the current git tag. +/// +@_spi(Internal) +@DependencyClient +public struct GitClient: Sendable { + + /// The closure to run that returns the current version from a given + /// git directory. + @available(*, deprecated, message: "Use version.") + public var currentVersion: @Sendable (String?, Bool) async throws -> String + + /// Get the current version from the `git tag` in the given directory. + /// If a directory is not passed in, then we will use the current working directory. + /// + /// - Parameters: + /// - gitDirectory: The directory to run the command in. + @available(*, deprecated, message: "Use version.") + public func currentVersion(in gitDirectory: String? = nil, exactMatch: Bool = true) async throws -> String { + try await currentVersion(gitDirectory, exactMatch) + } + + public var version: (CurrentVersionOption) async throws -> Version + + public struct CurrentVersionOption: Sendable { + let gitDirectory: String? + let style: Style + + public init( + gitDirectory: String? = nil, + style: Style + ) { + self.gitDirectory = gitDirectory + self.style = style + } + + public enum Style: Sendable { + case tag(exactMatch: Bool = false) + case branch(commitSha: Bool = true) + } + + } + + public enum Version: Sendable, CustomStringConvertible { + case branch(String) + case tag(String) + + public var description: String { + switch self { + case let .branch(string): return string + case let .tag(string): return string + } + } + } +} + +@_spi(Internal) +extension GitClient: TestDependencyKey { + + /// The ``GitVersionClient`` used in test / debug builds. + public static let testValue = GitClient() + + /// The ``GitVersionClient`` used in release builds. + public static var liveValue: GitClient { + .init( + currentVersion: { gitDirectory, exactMatch in + try await GitVersion(workingDirectory: gitDirectory).currentVersion(exactMatch) + }, + version: { try await $0.run() } + ) + } + + /// Create a mock git client, that always returns the given value. + /// + /// - Parameters: + /// - value: The value to return. + public static func mock(_ value: Version) -> Self { + .init( + currentVersion: { _, _ in value.description }, + version: { _ in value } + ) + } +} + +// MARK: - Private + +private extension GitClient.CurrentVersionOption { + + func run() async throws -> GitClient.Version { + switch style { + case let .tag(exactMatch: exactMatch): + return try await .tag(runCommand(.describeTag(exactMatch: exactMatch))) + case let .branch(commitSha: withCommit): + async let branch = try await runCommand(.branch) + + if withCommit { + let commit = try await runCommand(.commit) + return try await .branch("\(branch)-\(commit)") + } + return try await .branch(branch) + } + } + + func runCommand(_ versionArgs: VersionArgs) async throws -> String { + @Dependency(\.asyncShellClient) var shell + @Dependency(\.fileClient) var fileClient + + var gitDirectory: String! = self.gitDirectory + if gitDirectory == nil { + gitDirectory = try await fileClient.currentDirectory() + } + + return try await shell.background( + .init( + shell: .env, + environment: ProcessInfo.processInfo.environment, + in: gitDirectory, + versionArgs().map(\.rawValue) + ), + trimmingCharactersIn: .whitespacesAndNewlines + ) + } + + enum VersionArgs { + case branch + case commit + case describeTag(exactMatch: Bool) + + func callAsFunction() -> [Args] { + switch self { + case .branch: + return [.git, .symbolicRef, .quiet, .short, .head] + case .commit: + return [.git, .revParse, .short, .head] + case let .describeTag(exactMatch): + var args = [Args.git, .describe, .tags] + if exactMatch { + args.append(.exactMatch) + } + return args + } + } + + enum Args: String, CustomStringConvertible { + case git + case describe + case tags = "--tags" + case exactMatch = "--exact-match" + case quiet = "--quiet" + case symbolicRef = "symbolic-ref" + case revParse = "rev-parse" + case short = "--short" + case head = "HEAD" + } + + } + +} + +private struct GitVersion { + @Dependency(\.logger) var logger: Logger + @Dependency(\.asyncShellClient) var shell + + let workingDirectory: String? + + func currentVersion(_ exactMatch: Bool) async throws -> String { + logger.debug("\("Fetching current version".bold)") + do { + logger.debug("Checking for tag.") + return try await run(command: command(for: .describe(exactMatch: exactMatch))) + } catch { + logger.debug("\("No tag found, deferring to branch & git sha".red)") + let branch = try await run(command: command(for: .branch)) + let commit = try await run(command: command(for: .commit)) + return "\(branch) \(commit)" + } + } + + func command(for argument: VersionArgs) -> ShellCommand { + .init( + shell: .env, + environment: nil, + in: workingDirectory ?? FileManager.default.currentDirectoryPath, + argument.arguments.map(\.rawValue) + ) + } + + func run(command: ShellCommand) async throws -> String { + try await shell.background(command, trimmingCharactersIn: .whitespacesAndNewlines) + } + + enum VersionArgs { + case branch + case commit + case describe(exactMatch: Bool) + + var arguments: [Args] { + switch self { + case .branch: + return [.git, .symbolicRef, .quiet, .short, .head] + case .commit: + return [.git, .revParse, .short, .head] + case let .describe(exactMatch): + var args = [Args.git, .describe, .tags] + if exactMatch { + args.append(.exactMatch) + } + return args + } + } + + enum Args: String, CustomStringConvertible { + case git + case describe + case tags = "--tags" + case exactMatch = "--exact-match" + case quiet = "--quiet" + case symbolicRef = "symbolic-ref" + case revParse = "rev-parse" + case short = "--short" + case head = "HEAD" + } + + } +} + +extension RawRepresentable where RawValue == String, Self: CustomStringConvertible { + var description: String { rawValue } +} + +@_spi(Internal) +public extension GitClient.Version { + static var mocks: [Self] { + [ + .tag("1.0.0"), + .tag("1.0.0-4-g59bc977"), + .branch("dev-g59bc977") + ] + } +} diff --git a/Sources/CliVersion/GitVersionClient.swift b/Sources/CliVersion/GitVersionClient.swift deleted file mode 100644 index c2e9985..0000000 --- a/Sources/CliVersion/GitVersionClient.swift +++ /dev/null @@ -1,130 +0,0 @@ -import Foundation -#if canImport(FoundationNetworking) - import FoundationNetworking -#endif -import Dependencies -import DependenciesMacros -import ShellClient - -// TODO: This can be an internal dependency. -public extension DependencyValues { - - /// A ``GitVersionClient`` that can retrieve the current version from a - /// git directory. - var gitVersionClient: GitVersionClient { - get { self[GitVersionClient.self] } - set { self[GitVersionClient.self] = newValue } - } -} - -/// A client that can retrieve the current version from a git directory. -/// It will use the current `tag`, or if the current git tree does not -/// point to a commit that is tagged, it will use the `branch git-sha` as -/// the version. -/// -/// This is often not used directly, instead it is used with one of the plugins -/// that is supplied with this library. The use case is to set the version of a command line -/// tool based on the current git tag. -/// -@DependencyClient -public struct GitVersionClient: Sendable { - - /// The closure to run that returns the current version from a given - /// git directory. - public var currentVersion: @Sendable (String?, Bool) async throws -> String - - /// Get the current version from the `git tag` in the given directory. - /// If a directory is not passed in, then we will use the current working directory. - /// - /// - Parameters: - /// - gitDirectory: The directory to run the command in. - public func currentVersion(in gitDirectory: String? = nil, exactMatch: Bool = true) async throws -> String { - try await currentVersion(gitDirectory, exactMatch) - } -} - -extension GitVersionClient: TestDependencyKey { - - /// The ``GitVersionClient`` used in test / debug builds. - public static let testValue = GitVersionClient() - - /// The ``GitVersionClient`` used in release builds. - public static var liveValue: GitVersionClient { - .init(currentVersion: { gitDirectory, exactMatch in - try await GitVersion(workingDirectory: gitDirectory).currentVersion(exactMatch) - }) - } -} - -// MARK: - Private - -private struct GitVersion { - @Dependency(\.logger) var logger: Logger - @Dependency(\.asyncShellClient) var shell - - let workingDirectory: String? - - func currentVersion(_ exactMatch: Bool) async throws -> String { - logger.debug("\("Fetching current version".bold)") - do { - logger.debug("Checking for tag.") - return try await run(command: command(for: .describe(exactMatch: exactMatch))) - } catch { - logger.debug("\("No tag found, deferring to branch & git sha".red)") - let branch = try await run(command: command(for: .branch)) - let commit = try await run(command: command(for: .commit)) - return "\(branch) \(commit)" - } - } - - func command(for argument: VersionArgs) -> ShellCommand { - .init( - shell: .env, - environment: nil, - in: workingDirectory ?? FileManager.default.currentDirectoryPath, - argument.arguments.map(\.rawValue) - ) - } - - func run(command: ShellCommand) async throws -> String { - try await shell.background(command, trimmingCharactersIn: .whitespacesAndNewlines) - } - - enum VersionArgs { - case branch - case commit - case describe(exactMatch: Bool) - - var arguments: [Args] { - switch self { - case .branch: - return [.git, .symbolicRef, .quiet, .short, .head] - case .commit: - return [.git, .revParse, .short, .head] - case let .describe(exactMatch): - var args = [Args.git, .describe, .tags] - if exactMatch { - args.append(.exactMatch) - } - return args - } - } - - enum Args: String, CustomStringConvertible { - case git - case describe - case tags = "--tags" - case exactMatch = "--exact-match" - case quiet = "--quiet" - case symbolicRef = "symbolic-ref" - case revParse = "rev-parse" - case short = "--short" - case head = "HEAD" - } - - } -} - -extension RawRepresentable where RawValue == String, Self: CustomStringConvertible { - var description: String { rawValue } -} diff --git a/Sources/CliVersion/SemVar.swift b/Sources/CliVersion/SemVar.swift new file mode 100644 index 0000000..1a6cb6c --- /dev/null +++ b/Sources/CliVersion/SemVar.swift @@ -0,0 +1,108 @@ +import Foundation + +// Container for sem-var version. +@_spi(Internal) +public struct SemVar: CustomStringConvertible, Equatable, Sendable { + /// The major version. + public let major: Int + /// The minor version. + public let minor: Int + /// The patch version. + public let patch: Int + /// Extra pre-release tag. + public let preRelease: String? + + public init( + major: Int, + minor: Int, + patch: Int, + preRelease: String? = nil + ) { + self.major = major + self.minor = minor + self.patch = patch + self.preRelease = preRelease + } + + public init(preRelease: String? = nil) { + self.init( + major: 0, + minor: 0, + patch: 0, + preRelease: preRelease + ) + } + + public init?(string: String) { + let parts = string.split(separator: ".") + guard parts.count >= 3 else { + return nil + } + let major = Int(String(parts[0].replacingOccurrences(of: "\"", with: ""))) + let minor = Int(String(parts[1])) + + let patchParts = parts[2].split(separator: "-") + let patch = Int(patchParts.first ?? "0") + let preRelease = String(patchParts.dropFirst().joined(separator: "-")) + + self.init( + major: major ?? 0, + minor: minor ?? 0, + patch: patch ?? 0, + preRelease: preRelease + ) + } + + public var description: String { versionString() } + + // Create a version string, optionally appending a suffix. + public func versionString(withPreReleaseTag: Bool = true) -> String { + let string = "\(major).\(minor).\(patch)" + + guard withPreReleaseTag else { return string } + + guard let suffix = preRelease, suffix.count > 0 else { + return string + } + + if !suffix.hasPrefix("-") { + return "\(string)-\(suffix)" + } + + return "\(string)\(suffix)" + } + + // Bumps the sem-var by the given option (major, minor, patch) + public func bump(_ option: CliClient.BumpOption) -> Self { + switch option { + case .major: + return .init( + major: major + 1, + minor: 0, + patch: 0, + preRelease: preRelease + ) + case .minor: + return .init( + major: major, + minor: minor + 1, + patch: 0, + preRelease: preRelease + ) + case .patch: + return .init( + major: major, + minor: minor, + patch: patch + 1, + preRelease: preRelease + ) + } + } +} + +@_spi(Internal) +public extension GitClient.Version { + var semVar: SemVar? { + .init(string: description) + } +} diff --git a/Sources/cli-version/GlobalOptions.swift b/Sources/cli-version/GlobalOptions.swift index b0e438e..e719186 100644 --- a/Sources/cli-version/GlobalOptions.swift +++ b/Sources/cli-version/GlobalOptions.swift @@ -70,7 +70,6 @@ extension GlobalOptions { .init( gitDirectory: gitDirectory, dryRun: dryRun, - fileName: fileName, target: target, logLevel: .init(verbose: verbose) ) @@ -79,7 +78,7 @@ extension GlobalOptions { func run(_ operation: () async throws -> Void) async throws { try await withDependencies { $0.fileClient = .liveValue - $0.gitVersionClient = .liveValue + $0.gitClient = .liveValue $0.cliClient = .liveValue } operation: { try await operation() diff --git a/Tests/CliVersionTests/CliClientTests.swift b/Tests/CliVersionTests/CliClientTests.swift index 26bc49a..aa4daa8 100644 --- a/Tests/CliVersionTests/CliClientTests.swift +++ b/Tests/CliVersionTests/CliClientTests.swift @@ -15,7 +15,7 @@ struct CliClientTests { try await run { @Dependency(\.cliClient) var client let output = try await client.build(.testOptions(target: target)) - #expect(output == "/baz/Sources/bar/foo") + #expect(output == "/baz/Sources/bar/Version.swift") } } @@ -30,7 +30,7 @@ struct CliClientTests { } operation: { @Dependency(\.cliClient) var client let output = try await client.bump(type, .testOptions()) - #expect(output == "/baz/Sources/bar/foo") + #expect(output == "/baz/Sources/bar/Version.swift") } assert: { string, _ in #expect(string != nil) @@ -54,7 +54,7 @@ struct CliClientTests { try await run { @Dependency(\.cliClient) var client let output = try await client.generate(.testOptions(target: target)) - #expect(output == "/baz/Sources/bar/foo") + #expect(output == "/baz/Sources/bar/Version.swift") } } @@ -67,7 +67,7 @@ struct CliClientTests { } operation: { @Dependency(\.cliClient) var client let output = try await client.update(.testOptions(dryRun: dryRun, target: target)) - #expect(output == "/baz/Sources/bar/foo") + #expect(output == "/baz/Sources/bar/Version.swift") } assert: { string, _ in if dryRun { #expect(string == nil) @@ -75,6 +75,18 @@ struct CliClientTests { } } + @Test(arguments: GitClient.Version.mocks) + func gitVersionToSemVar(version: GitClient.Version) { + let semVar = version.semVar + if semVar != nil { + #expect(semVar!.versionString(allowPrerelease: false) == "1.0.0") + #expect(semVar!.versionString(allowPrerelease: true) == version.description) + } else { + let semVar = SemVar(preRelease: version.description) + #expect(semVar.versionString(allowPrerelease: true) == "0.0.0-\(version.description)") + } + } + func run( setupDependencies: @escaping (inout DependencyValues) -> Void = { _ in }, operation: @Sendable @escaping () async throws -> Void, @@ -86,7 +98,7 @@ struct CliClientTests { $0.logger.logLevel = .debug $0.fileClient = .capturing(captured) $0.fileClient.fileExists = { _ in false } - $0.gitVersionClient = .init { _, _ in "1.0.0" } + $0.gitClient = .mock(.tag("1.0.0")) $0.cliClient = .liveValue setupDependencies(&$0) } operation: { @@ -120,14 +132,12 @@ extension CliClient.SharedOptions { static func testOptions( gitDirectory: String? = "/baz", dryRun: Bool = false, - fileName: String = "foo", target: String = "bar", logLevel: Logger.Level = .trace ) -> Self { .init( gitDirectory: gitDirectory, dryRun: dryRun, - fileName: fileName, target: target, logLevel: logLevel ) diff --git a/Tests/CliVersionTests/CliVersionTests.swift b/Tests/CliVersionTests/CliVersionTests.swift index f979ba2..bb855aa 100644 --- a/Tests/CliVersionTests/CliVersionTests.swift +++ b/Tests/CliVersionTests/CliVersionTests.swift @@ -11,7 +11,7 @@ final class GitVersionTests: XCTestCase { $0.logger.logLevel = .debug $0.logger = .liveValue $0.asyncShellClient = .liveValue - $0.gitVersionClient = .liveValue + $0.gitClient = .liveValue $0.fileClient = .liveValue }, operation: { super.invokeTest() @@ -27,7 +27,7 @@ final class GitVersionTests: XCTestCase { } func test_live() async throws { - @Dependency(\.gitVersionClient) var versionClient: GitVersionClient + @Dependency(\.gitClient) var versionClient: GitClient let version = try await versionClient.currentVersion(in: gitDir) print("VERSION: \(version)") @@ -35,24 +35,6 @@ final class GitVersionTests: XCTestCase { XCTAssertNotEqual(version, "blob") } - // func test_commands() throws { - // @Dependency(\.asyncShellClient) var shellClient: ShellClient - // - // XCTAssertNoThrow( - // try shellClient.background( - // .gitCurrentBranch(gitDirectory: gitDir), - // trimmingCharactersIn: .whitespacesAndNewlines - // ) - // ) - // - // XCTAssertNoThrow( - // try shellClient.background( - // .gitCurrentSha(gitDirectory: gitDir), - // trimmingCharactersIn: .whitespacesAndNewlines - // ) - // ) - // } - func test_file_client() async throws { try await withTemporaryDirectory { tmpDir in @Dependency(\.fileClient) var fileClient diff --git a/justfile b/justfile index 7bee14b..697edf5 100644 --- a/justfile +++ b/justfile @@ -1,9 +1,20 @@ +product := cli-version + +[private] +default: + @just --list build configuration="release": - @swift build --configuration {{configuration}} + @swift build \ + --disable-sandbox \ + --configuration {{configuration}} \ + --product {{product}} run *ARGS: - @swift run cli-version {{ARGS}} + @swift run {{product}} {{ARGS}} clean: rm -rf .build + +test *ARGS: + @swift test {{ARGS}}