feat: Updates dependencies and cli commands to be async.
This commit is contained in:
@@ -21,6 +21,7 @@ public struct CliClient: Sendable {
|
||||
/// Build and update the version based on the git tag, or branch + sha.
|
||||
public var build: @Sendable (SharedOptions) async throws -> String
|
||||
|
||||
/// Bump the existing version.
|
||||
public var bump: @Sendable (BumpOption, SharedOptions) async throws -> String
|
||||
|
||||
/// Generate a version file with an optional version that can be set manually.
|
||||
@@ -63,10 +64,10 @@ extension CliClient: DependencyKey {
|
||||
|
||||
public static func live(environment: [String: String]) -> Self {
|
||||
.init(
|
||||
build: { try $0.build(environment) },
|
||||
bump: { try $1.bump($0) },
|
||||
generate: { try $0.generate() },
|
||||
update: { try $0.update() }
|
||||
build: { try await $0.build(environment) },
|
||||
bump: { try await $1.bump($0) },
|
||||
generate: { try await $0.generate() },
|
||||
update: { try await $0.update() }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -97,20 +98,20 @@ public extension CliClient.SharedOptions {
|
||||
|
||||
@discardableResult
|
||||
func run<T>(
|
||||
_ operation: () throws -> T
|
||||
) rethrows -> T {
|
||||
try withDependencies {
|
||||
_ operation: () async throws -> T
|
||||
) async rethrows -> T {
|
||||
try await withDependencies {
|
||||
$0.logger.logLevel = .init(verbose: verbose)
|
||||
} operation: {
|
||||
try operation()
|
||||
try await operation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension CliClient.SharedOptions {
|
||||
|
||||
func build(_ environment: [String: String]) throws -> String {
|
||||
try run {
|
||||
func build(_ environment: [String: String]) async throws -> String {
|
||||
try await run {
|
||||
@Dependency(\.gitVersionClient) var gitVersion
|
||||
@Dependency(\.fileClient) var fileClient
|
||||
@Dependency(\.logger) var logger
|
||||
@@ -126,91 +127,95 @@ private extension CliClient.SharedOptions {
|
||||
let fileUrl = self.fileUrl
|
||||
logger.debug("File url: \(fileUrl.cleanFilePath)")
|
||||
|
||||
let currentVersion = try gitVersion.currentVersion(in: gitDirectory)
|
||||
let currentVersion = try await gitVersion.currentVersion(in: gitDirectory)
|
||||
|
||||
let fileContents = Template.build(currentVersion)
|
||||
|
||||
try fileClient.write(string: fileContents, to: fileUrl)
|
||||
try await fileClient.write(string: fileContents, to: fileUrl)
|
||||
|
||||
return fileUrl.cleanFilePath
|
||||
}
|
||||
}
|
||||
|
||||
func bump(_ type: CliClient.BumpOption) throws -> String {
|
||||
@Dependency(\.fileClient) var fileClient
|
||||
@Dependency(\.logger) var logger
|
||||
func bump(_ type: CliClient.BumpOption) async throws -> String {
|
||||
try await run {
|
||||
@Dependency(\.fileClient) var fileClient
|
||||
@Dependency(\.logger) var logger
|
||||
|
||||
let targetUrl = fileUrl
|
||||
let targetUrl = fileUrl
|
||||
|
||||
logger.debug("Bump target url: \(targetUrl.cleanFilePath)")
|
||||
logger.debug("Bump target url: \(targetUrl.cleanFilePath)")
|
||||
|
||||
let contents = try fileClient.read(fileUrl.cleanFilePath)
|
||||
let versionLine = contents.split(separator: "\n")
|
||||
.first { $0.hasPrefix("let VERSION:") }
|
||||
let contents = try await fileClient.read(fileUrl)
|
||||
let versionLine = contents.split(separator: "\n")
|
||||
.first { $0.hasPrefix("let VERSION:") }
|
||||
|
||||
guard let versionLine else {
|
||||
throw CliClientError.failedToParseVersionFile
|
||||
guard let versionLine else {
|
||||
throw CliClientError.failedToParseVersionFile
|
||||
}
|
||||
|
||||
let isOptional = versionLine.contains("String?")
|
||||
let versionString = versionLine.split(separator: "let VERSION: \(isOptional ? "String?" : "String") = ").last
|
||||
guard let versionString else {
|
||||
throw CliClientError.failedToParseVersionFile
|
||||
}
|
||||
|
||||
let parts = String(versionString).split(separator: ".")
|
||||
logger.debug("Version parts: \(parts)")
|
||||
|
||||
// TODO: Better error.
|
||||
guard parts.count == 3 else {
|
||||
throw CliClientError.failedToParseVersionFile
|
||||
}
|
||||
|
||||
var major = Int(String(parts[0])) ?? 0
|
||||
var minor = Int(String(parts[1])) ?? 0
|
||||
var patch = Int(String(parts[2])) ?? 0
|
||||
|
||||
type.bump(major: &major, minor: &minor, patch: &patch)
|
||||
|
||||
let version = "\(major).\(minor).\(patch)"
|
||||
logger.debug("Bumped version: \(version)")
|
||||
|
||||
let template = isOptional ? Template.optional(version) : Template.build(version)
|
||||
|
||||
if !dryRun {
|
||||
try await fileClient.write(string: template, to: targetUrl)
|
||||
} else {
|
||||
logger.debug("Skipping, due to dry-run being passed.")
|
||||
}
|
||||
|
||||
return targetUrl.cleanFilePath
|
||||
}
|
||||
|
||||
let isOptional = versionLine.contains("String?")
|
||||
let versionString = versionLine.split(separator: "let VERSION: \(isOptional ? "String?" : "String") = ").last
|
||||
guard let versionString else {
|
||||
throw CliClientError.failedToParseVersionFile
|
||||
}
|
||||
|
||||
let parts = String(versionString).split(separator: ".")
|
||||
logger.debug("Version parts: \(parts)")
|
||||
|
||||
// TODO: Better error.
|
||||
guard parts.count == 3 else {
|
||||
throw CliClientError.failedToParseVersionFile
|
||||
}
|
||||
|
||||
var major = Int(String(parts[0])) ?? 0
|
||||
var minor = Int(String(parts[1])) ?? 0
|
||||
var patch = Int(String(parts[2])) ?? 0
|
||||
|
||||
type.bump(major: &major, minor: &minor, patch: &patch)
|
||||
|
||||
let version = "\(major).\(minor).\(patch)"
|
||||
logger.debug("Bumped version: \(version)")
|
||||
|
||||
let template = isOptional ? Template.optional(version) : Template.build(version)
|
||||
|
||||
if !dryRun {
|
||||
try fileClient.write(string: template, to: targetUrl)
|
||||
} else {
|
||||
logger.debug("Skipping, due to dry-run being passed.")
|
||||
}
|
||||
|
||||
return targetUrl.cleanFilePath
|
||||
}
|
||||
|
||||
func generate(_ version: String? = nil) throws -> String {
|
||||
@Dependency(\.fileClient) var fileClient
|
||||
@Dependency(\.logger) var logger
|
||||
func generate(_ version: String? = nil) async throws -> String {
|
||||
try await run {
|
||||
@Dependency(\.fileClient) var fileClient
|
||||
@Dependency(\.logger) var logger
|
||||
|
||||
let targetUrl = fileUrl
|
||||
let targetUrl = fileUrl
|
||||
|
||||
logger.debug("Generate target url: \(targetUrl.cleanFilePath)")
|
||||
logger.debug("Generate target url: \(targetUrl.cleanFilePath)")
|
||||
|
||||
guard !fileClient.fileExists(targetUrl) else {
|
||||
throw CliClientError.fileExists(path: targetUrl.cleanFilePath)
|
||||
guard !fileClient.fileExists(targetUrl) else {
|
||||
throw CliClientError.fileExists(path: targetUrl.cleanFilePath)
|
||||
}
|
||||
|
||||
let template = Template.optional(version)
|
||||
|
||||
if !dryRun {
|
||||
try await fileClient.write(string: template, to: targetUrl)
|
||||
} else {
|
||||
logger.debug("Skipping, due to dry-run being passed.")
|
||||
}
|
||||
return targetUrl.cleanFilePath
|
||||
}
|
||||
|
||||
let template = Template.optional(version)
|
||||
|
||||
if !dryRun {
|
||||
try fileClient.write(string: template, to: targetUrl)
|
||||
} else {
|
||||
logger.debug("Skipping, due to dry-run being passed.")
|
||||
}
|
||||
return targetUrl.cleanFilePath
|
||||
}
|
||||
|
||||
func update() throws -> String {
|
||||
func update() async throws -> String {
|
||||
@Dependency(\.gitVersionClient) var gitVersionClient
|
||||
return try generate(gitVersionClient.currentVersion(in: gitDirectory))
|
||||
return try await generate(gitVersionClient.currentVersion(in: gitDirectory))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import Foundation
|
||||
#endif
|
||||
import XCTestDynamicOverlay
|
||||
|
||||
// TODO: Need a capturing version on write for tests.
|
||||
|
||||
public extension DependencyValues {
|
||||
|
||||
/// Access a basic ``FileClient`` that can read / write data to the file system.
|
||||
@@ -27,20 +29,21 @@ public extension DependencyValues {
|
||||
@DependencyClient
|
||||
public struct FileClient: Sendable {
|
||||
|
||||
/// Check if a file exists at the given url.
|
||||
public var fileExists: @Sendable (URL) -> Bool = { _ in true }
|
||||
|
||||
/// Read the contents of a file.
|
||||
public var read: @Sendable (URL) throws -> String
|
||||
public var read: @Sendable (URL) async throws -> String
|
||||
|
||||
/// Write `Data` to a file `URL`.
|
||||
public var write: @Sendable (Data, URL) throws -> Void
|
||||
public var write: @Sendable (Data, URL) async throws -> Void
|
||||
|
||||
/// Read the contents of a file at the given path.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - path: The file path to read from.
|
||||
public func read(_ path: String) throws -> String {
|
||||
try read(url(for: path))
|
||||
public func read(_ path: String) async throws -> String {
|
||||
try await read(url(for: path))
|
||||
}
|
||||
|
||||
/// Write's the the string to a file path.
|
||||
@@ -48,9 +51,8 @@ public struct FileClient: Sendable {
|
||||
/// - Parameters:
|
||||
/// - string: The string to write to the file.
|
||||
/// - path: The file path.
|
||||
public func write(string: String, to path: String) throws {
|
||||
let url = url(for: path)
|
||||
try write(string: string, to: url)
|
||||
public func write(string: String, to path: String) async throws {
|
||||
try await write(string: string, to: url(for: path))
|
||||
}
|
||||
|
||||
/// Write's the the string to a file path.
|
||||
@@ -58,8 +60,8 @@ public struct FileClient: Sendable {
|
||||
/// - Parameters:
|
||||
/// - string: The string to write to the file.
|
||||
/// - url: The file url.
|
||||
public func write(string: String, to url: URL) throws {
|
||||
try write(Data(string.utf8), url)
|
||||
public func write(string: String, to url: URL) async throws {
|
||||
try await write(Data(string.utf8), url)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,3 +85,15 @@ extension FileClient: DependencyKey {
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
private actor CapturingWrite {
|
||||
var data: Data?
|
||||
var url: URL?
|
||||
|
||||
init() {}
|
||||
|
||||
func set(data: Data, url: URL) {
|
||||
self.data = data
|
||||
self.url = url
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import Foundation
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
import Dependencies
|
||||
import DependenciesMacros
|
||||
import ShellClient
|
||||
import XCTestDynamicOverlay
|
||||
|
||||
@@ -14,57 +16,32 @@ import XCTestDynamicOverlay
|
||||
/// 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.
|
||||
///
|
||||
public struct GitVersionClient {
|
||||
@DependencyClient
|
||||
public struct GitVersionClient: Sendable {
|
||||
|
||||
/// The closure to run that returns the current version from a given
|
||||
/// git directory.
|
||||
private var currentVersion: (String?) throws -> String
|
||||
|
||||
/// Create a new ``GitVersionClient`` instance.
|
||||
///
|
||||
/// This is normally not interacted with directly, instead access the client through the dependency system.
|
||||
/// ```swift
|
||||
/// @Dependency(\.gitVersionClient)
|
||||
/// ```
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - currentVersion: The closure that returns the current version.
|
||||
///
|
||||
public init(currentVersion: @escaping (String?) throws -> String) {
|
||||
self.currentVersion = currentVersion
|
||||
}
|
||||
public var currentVersion: @Sendable (String?) 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) throws -> String {
|
||||
try currentVersion(gitDirectory)
|
||||
}
|
||||
|
||||
/// Override the `currentVersion` command and return the passed in version string.
|
||||
///
|
||||
/// This is useful for testing purposes.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - version: The version string to return when `currentVersion` is called.
|
||||
public mutating func override(with version: String) {
|
||||
currentVersion = { _ in version }
|
||||
public func currentVersion(in gitDirectory: String? = nil) async throws -> String {
|
||||
try await currentVersion(gitDirectory)
|
||||
}
|
||||
}
|
||||
|
||||
extension GitVersionClient: TestDependencyKey {
|
||||
|
||||
/// The ``GitVersionClient`` used in test / debug builds.
|
||||
public static let testValue = GitVersionClient(
|
||||
currentVersion: unimplemented("\(Self.self).currentVersion", placeholder: "")
|
||||
)
|
||||
public static let testValue = GitVersionClient()
|
||||
|
||||
/// The ``GitVersionClient`` used in release builds.
|
||||
public static var liveValue: GitVersionClient {
|
||||
.init(currentVersion: { gitDirectory in
|
||||
try GitVersion(workingDirectory: gitDirectory).currentVersion()
|
||||
try await GitVersion(workingDirectory: gitDirectory).currentVersion()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -97,19 +74,19 @@ public extension ShellCommand {
|
||||
|
||||
private struct GitVersion {
|
||||
@Dependency(\.logger) var logger: Logger
|
||||
@Dependency(\.shellClient) var shell: ShellClient
|
||||
@Dependency(\.asyncShellClient) var shell
|
||||
|
||||
let workingDirectory: String?
|
||||
|
||||
func currentVersion() throws -> String {
|
||||
func currentVersion() async throws -> String {
|
||||
logger.debug("\("Fetching current version".bold)")
|
||||
do {
|
||||
logger.debug("Checking for tag.")
|
||||
return try run(command: command(for: .describe))
|
||||
return try await run(command: command(for: .describe))
|
||||
} catch {
|
||||
logger.debug("\("No tag found, deferring to branch & git sha".red)")
|
||||
let branch = try run(command: command(for: .branch))
|
||||
let commit = try run(command: command(for: .commit))
|
||||
let branch = try await run(command: command(for: .branch))
|
||||
let commit = try await run(command: command(for: .commit))
|
||||
return "\(branch) \(commit)"
|
||||
}
|
||||
}
|
||||
@@ -125,8 +102,8 @@ private struct GitVersion {
|
||||
}
|
||||
|
||||
private extension GitVersion {
|
||||
func run(command: ShellCommand) throws -> String {
|
||||
try shell.background(command, trimmingCharactersIn: .whitespacesAndNewlines)
|
||||
func run(command: ShellCommand) async throws -> String {
|
||||
try await shell.background(command, trimmingCharactersIn: .whitespacesAndNewlines)
|
||||
}
|
||||
|
||||
enum VersionArgs {
|
||||
|
||||
@@ -9,14 +9,14 @@ import Foundation
|
||||
/// - Parameters:
|
||||
/// - operation: The operation to run with the temporary directory.
|
||||
public func withTemporaryDirectory(
|
||||
_ operation: (URL) throws -> Void
|
||||
) rethrows {
|
||||
_ operation: (URL) async throws -> Void
|
||||
) async rethrows {
|
||||
let tempUrl = FileManager.default
|
||||
.temporaryDirectory
|
||||
.appendingPathComponent(UUID().uuidString)
|
||||
|
||||
try! FileManager.default.createDirectory(at: tempUrl, withIntermediateDirectories: false)
|
||||
try operation(tempUrl)
|
||||
try await operation(tempUrl)
|
||||
try! FileManager.default.removeItem(at: tempUrl)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import Foundation
|
||||
import ShellClient
|
||||
|
||||
extension CliVersionCommand {
|
||||
struct Build: ParsableCommand {
|
||||
struct Build: AsyncParsableCommand {
|
||||
static var configuration: CommandConfiguration = .init(
|
||||
abstract: "Used for the build with version plugin.",
|
||||
discussion: "This should generally not be interacted with directly, outside of the build plugin."
|
||||
@@ -19,8 +19,8 @@ extension CliVersionCommand {
|
||||
var gitDirectory: String
|
||||
|
||||
// TODO: Use CliClient
|
||||
func run() throws {
|
||||
try withDependencies {
|
||||
func run() async throws {
|
||||
try await withDependencies {
|
||||
$0.logger.logLevel = .debug
|
||||
$0.fileClient = .liveValue
|
||||
$0.gitVersionClient = .liveValue
|
||||
@@ -37,12 +37,12 @@ extension CliVersionCommand {
|
||||
let fileString = fileUrl.fileString()
|
||||
logger.info("File Url: \(fileString)")
|
||||
|
||||
let currentVersion = try gitVersion.currentVersion(in: gitDirectory)
|
||||
let currentVersion = try await gitVersion.currentVersion(in: gitDirectory)
|
||||
|
||||
let fileContents = buildTemplate
|
||||
.replacingOccurrences(of: "nil", with: "\"\(currentVersion)\"")
|
||||
|
||||
try fileClient.write(string: fileContents, to: fileUrl)
|
||||
try await fileClient.write(string: fileContents, to: fileUrl)
|
||||
logger.info("Updated version file: \(fileString)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@ import ArgumentParser
|
||||
import Foundation
|
||||
|
||||
@main
|
||||
struct CliVersionCommand: ParsableCommand {
|
||||
static var configuration: CommandConfiguration = .init(
|
||||
commandName: "cli-version",
|
||||
version: VERSION ?? "0.0.0",
|
||||
subcommands: [
|
||||
Build.self,
|
||||
Generate.self,
|
||||
Update.self,
|
||||
]
|
||||
)
|
||||
struct CliVersionCommand: AsyncParsableCommand {
|
||||
static var configuration: CommandConfiguration = .init(
|
||||
commandName: "cli-version",
|
||||
version: VERSION ?? "0.0.0",
|
||||
subcommands: [
|
||||
Build.self,
|
||||
Generate.self,
|
||||
Update.self
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import Foundation
|
||||
import ShellClient
|
||||
|
||||
extension CliVersionCommand {
|
||||
struct Generate: ParsableCommand {
|
||||
struct Generate: AsyncParsableCommand {
|
||||
static var configuration: CommandConfiguration = .init(
|
||||
abstract: "Generates a version file in a command line tool that can be set via the git tag or git sha.",
|
||||
discussion: "This command can be interacted with directly, outside of the plugin usage context.",
|
||||
@@ -15,7 +15,7 @@ extension CliVersionCommand {
|
||||
@OptionGroup var shared: SharedOptions
|
||||
|
||||
// TODO: Use CliClient
|
||||
func run() throws {
|
||||
func run() async throws {
|
||||
@Dependency(\.logger) var logger: Logger
|
||||
@Dependency(\.fileClient) var fileClient
|
||||
|
||||
@@ -30,7 +30,7 @@ extension CliVersionCommand {
|
||||
}
|
||||
|
||||
if !shared.dryRun {
|
||||
try fileClient.write(string: optionalTemplate, to: fileUrl)
|
||||
try await fileClient.write(string: optionalTemplate, to: fileUrl)
|
||||
logger.info("Generated file at: \(fileString)")
|
||||
} else {
|
||||
logger.info("Would generate file at: \(fileString)")
|
||||
|
||||
@@ -6,7 +6,7 @@ import ShellClient
|
||||
|
||||
extension CliVersionCommand {
|
||||
|
||||
struct Update: ParsableCommand {
|
||||
struct Update: AsyncParsableCommand {
|
||||
static var configuration: CommandConfiguration = .init(
|
||||
abstract: "Updates a version string to the git tag or git sha.",
|
||||
discussion: "This command can be interacted with directly outside of the plugin context."
|
||||
@@ -21,17 +21,17 @@ extension CliVersionCommand {
|
||||
var gitDirectory: String?
|
||||
|
||||
// TODO: Use CliClient
|
||||
func run() throws {
|
||||
try withDependencies {
|
||||
func run() async throws {
|
||||
try await withDependencies {
|
||||
$0.logger.logLevel = shared.verbose ? .debug : .info
|
||||
$0.fileClient = .liveValue
|
||||
$0.gitVersionClient = .liveValue
|
||||
$0.shellClient = .liveValue
|
||||
$0.asyncShellClient = .liveValue
|
||||
} operation: {
|
||||
@Dependency(\.gitVersionClient) var gitVersion
|
||||
@Dependency(\.fileClient) var fileClient
|
||||
@Dependency(\.logger) var logger
|
||||
@Dependency(\.shellClient) var shell
|
||||
@Dependency(\.asyncShellClient) var shell
|
||||
|
||||
let targetUrl = parseTarget(shared.target)
|
||||
let fileUrl = targetUrl
|
||||
@@ -39,13 +39,13 @@ extension CliVersionCommand {
|
||||
|
||||
let fileString = fileUrl.fileString()
|
||||
|
||||
let currentVersion = try gitVersion.currentVersion(in: gitDirectory)
|
||||
let currentVersion = try await gitVersion.currentVersion(in: gitDirectory)
|
||||
|
||||
let fileContents = optionalTemplate
|
||||
.replacingOccurrences(of: "nil", with: "\"\(currentVersion)\"")
|
||||
|
||||
if !shared.dryRun {
|
||||
try fileClient.write(string: fileContents, to: fileUrl)
|
||||
try await fileClient.write(string: fileContents, to: fileUrl)
|
||||
logger.info("Updated version file: \(fileString)")
|
||||
} else {
|
||||
logger.info("Would update file contents to:")
|
||||
|
||||
Reference in New Issue
Block a user