diff --git a/Makefile b/Makefile index bcf14a2..6b7a541 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ PLATFORM_MACOS = macOS CONFIG := debug -DOCC_TARGET ?= CliVersion +DOCC_TARGET ?= CliClient DOCC_BASEPATH = $(shell basename "$(PWD)") DOCC_DIR ?= ./docs SWIFT_VERSION ?= "5.10" diff --git a/Package.resolved b/Package.resolved index 79e9970..79f37da 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "077fe473b2dff48184d79b6897170a2c87f00a465e6c079889de37e6470001fb", + "originHash" : "6ab0a9c883cfa1490d249a344074ad27369033fab78e1a90272ef07339a8c0ab", "pins" : [ { "identity" : "combine-schedulers", @@ -105,8 +105,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/m-housh/swift-shell-client.git", "state" : { - "revision" : "ea819f41b87aa94e792f028a031f5db786e79a94", - "version" : "0.2.1" + "revision" : "d67f693f92428ef8adecb48c2b26ccac0d2f98cb", + "version" : "0.2.2" } }, { diff --git a/Package.swift b/Package.swift index f46c6c8..a559c3d 100644 --- a/Package.swift +++ b/Package.swift @@ -18,7 +18,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/pointfreeco/swift-dependencies.git", from: "1.6.2"), - .package(url: "https://github.com/m-housh/swift-shell-client.git", from: "0.2.0"), + .package(url: "https://github.com/m-housh/swift-shell-client.git", from: "0.2.2"), .package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.0.0"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.6.2"), @@ -75,6 +75,10 @@ let package = Package( .product(name: "ShellClient", package: "swift-shell-client") ] ), + .testTarget( + name: "GitClientTests", + dependencies: ["GitClient"] + ), .target(name: "TestSupport"), .plugin( name: "BuildWithVersionPlugin", diff --git a/Sources/CliClient/CliClient.swift b/Sources/CliClient/CliClient.swift index 236899e..438c84d 100644 --- a/Sources/CliClient/CliClient.swift +++ b/Sources/CliClient/CliClient.swift @@ -27,6 +27,9 @@ public struct CliClient: Sendable { /// Generate a version file with an optional version that can be set manually. public var generate: @Sendable (SharedOptions) async throws -> String + /// Parse the configuration options. + public var parsedConfiguration: @Sendable (SharedOptions) async throws -> Configuration + public enum BumpOption: Sendable, CaseIterable { case major, minor, patch, preRelease } @@ -94,7 +97,10 @@ extension CliClient: DependencyKey { .init( build: { try await $0.build(environment) }, bump: { try await $1.bump($0) }, - generate: { try await $0.generate() } + generate: { try await $0.generate() }, + parsedConfiguration: { options in + try await options.withMergedConfiguration { $0 } + } ) } diff --git a/Sources/CliClient/Documentation.docc/Articles/GettingStarted.md b/Sources/CliClient/Documentation.docc/Articles/GettingStarted.md index 2a7b613..9e7e763 100644 --- a/Sources/CliClient/Documentation.docc/Articles/GettingStarted.md +++ b/Sources/CliClient/Documentation.docc/Articles/GettingStarted.md @@ -7,25 +7,25 @@ Learn how to integrate the plugins into your project Use the plugins by including as a package to your project and declaring in the `plugins` section of your target. -> Note: You must use swift-tools version 5.6 or greater for package plugins and -> target `macOS(.v10_15)` or greater. +> Note: You must use swift-tools version 5.6 or greater for package plugins and target `macOS(.v13)` +> or greater. ```swift -// swift-tools-version: 5.7 +// swift-tools-version: 5.10 import PackageDescription let package = Package( - platforms:[.macOS(.v10_15)], + platforms:[.macOS(.v13)], dependencies: [ ..., - .package(url: "https://github.com/m-housh/swift-cli-version.git", from: "0.1.0") + .package(url: "https://github.com/m-housh/swift-cli-version.git", from: "0.2.0") ], targets: [ .executableTarget( name: "", dependencies: [...], - plugins: [ + plugins: [ .plugin(name: "BuildWithVersionPlugin", package: "swift-cli-version") ] ) @@ -33,8 +33,8 @@ let package = Package( ) ``` -The above example uses the build tool plugin. The `BuildWithVersionPlugin` will give you access -to a `VERSION` variable in your project that you can use to supply the version of the tool. +The above example uses the build tool plugin. The `BuildWithVersionPlugin` will give you access to a +`VERSION` variable in your project that you can use to supply the version of the tool. ### Example @@ -42,27 +42,27 @@ to a `VERSION` variable in your project that you can use to supply the version o import ArgumentParser @main -struct MyCliTool: ParsableCommand { +struct MyCliTool: ParsableCommand { static let configuration = CommandConfiguration( abstract: "My awesome cli tool", version: VERSION ) - func run() throws { + func run() throws { print("Version: \(VERSION)") } } ``` -After you enable the plugin, you will have access to the `VERSION` string variable even though it is +After you enable the plugin, you will have access to the `VERSION` string variable even though it is not declared in your source files. ![Trust & Enable](trust) > Note: If your `DerivedData` folder lives in a directory that is a mounted volume / or somewhere -> that is not under your home folder then you may get build failures using the build tool -> plugin, it will work if you build from the command line and pass the `--disable-sandbox` flag to the -> build command or use one of the manual methods. +> that is not under your home folder then you may get build failures using the build tool plugin, it +> will work if you build from the command line and pass the `--disable-sandbox` flag to the build +> command or use one of the manual methods. ## See Also diff --git a/Sources/CliClient/Documentation.docc/CliVersion.md b/Sources/CliClient/Documentation.docc/CliVersion.md index 4a0dcc3..6f17203 100644 --- a/Sources/CliClient/Documentation.docc/CliVersion.md +++ b/Sources/CliClient/Documentation.docc/CliVersion.md @@ -1,6 +1,6 @@ -# ``CliVersion`` +# CliClient -Derive a version for a command line tool from git tags or a git sha. +Derive a version for a command-line tool from git tags or a git sha. ## Additional Resources @@ -9,15 +9,10 @@ Derive a version for a command line tool from git tags or a git sha. ## Overview This tool exposes several plugins that can be used to derive a version for a command line program at -build time or by manually running the plugin. The version is derived from git tags and falling back to -the branch and git sha if a tag is not set for the current worktree state. +build time or by manually running the plugin. The version is derived from git tags and falling back +to the branch and git sha if a tag is not set for the current worktree state. ## Articles - - - -## Api - -- ``FileClient`` -- ``GitVersionClient`` diff --git a/Sources/CliClient/Internal/Internal.swift b/Sources/CliClient/Internal/Internal.swift index 1689405..a1d9db8 100644 --- a/Sources/CliClient/Internal/Internal.swift +++ b/Sources/CliClient/Internal/Internal.swift @@ -7,6 +7,25 @@ import GitClient @_spi(Internal) public extension CliClient.SharedOptions { + // Merges any configuration set via the passed in options. + @discardableResult + func withMergedConfiguration( + operation: (Configuration) async throws -> T + ) async throws -> T { + try await withConfiguration(path: configurationFile) { configuration in + var configuration = configuration + configuration = configuration.mergingTarget(target) + + if configuration.strategy?.branch != nil, let branch { + configuration = configuration.mergingStrategy(.branch(branch)) + } else if let semvar { + configuration = configuration.mergingStrategy(.semvar(semvar)) + } + + return try await operation(configuration) + } + } + @discardableResult func run( _ operation: (CurrentVersionContainer) async throws -> Void @@ -15,19 +34,9 @@ public extension CliClient.SharedOptions { $0.logger.logLevel = logLevel } operation: { // Load the default configuration, if it exists. - try await withConfiguration(path: configurationFile) { configuration in + try await withMergedConfiguration { configuration in @Dependency(\.logger) var logger - // Merge any configuration set from caller into default configuration. - var configuration = configuration - configuration = configuration.mergingTarget(target) - - if configuration.strategy?.branch != nil, let branch { - configuration = configuration.mergingStrategy(.branch(branch)) - } else if let semvar { - configuration = configuration.mergingStrategy(.semvar(semvar)) - } - logger.debug("Configuration: \(configuration)") // This will fail if the target url is not set properly. diff --git a/Sources/ConfigurationClient/Configuration.swift b/Sources/ConfigurationClient/Configuration.swift index 69ec78a..530b829 100644 --- a/Sources/ConfigurationClient/Configuration.swift +++ b/Sources/ConfigurationClient/Configuration.swift @@ -21,9 +21,11 @@ public struct Configuration: Codable, Equatable, Sendable { self.strategy = strategy } - public static var mock: Self { + public static var `default`: Self { .mock(module: "") } + + public static func mock(module: String = "cli-version") -> Self { .init( - target: .init(module: .init("cli-version")), + target: .init(module: .init(module)), strategy: .semvar(.init()) ) } diff --git a/Sources/ConfigurationClient/ConfigurationClient.swift b/Sources/ConfigurationClient/ConfigurationClient.swift index 3e44d69..f2ae1eb 100644 --- a/Sources/ConfigurationClient/ConfigurationClient.swift +++ b/Sources/ConfigurationClient/ConfigurationClient.swift @@ -30,7 +30,7 @@ public struct ConfigurationClient: Sendable { guard let url = try? await find(url) else { throw ConfigurationClientError.configurationNotFound } - return (try? await load(url)) ?? .mock + return (try? await load(url)) ?? .default } } diff --git a/Sources/cli-version/CliVersionCommand.swift b/Sources/cli-version/Application.swift similarity index 50% rename from Sources/cli-version/CliVersionCommand.swift rename to Sources/cli-version/Application.swift index 59f9883..3ad6c74 100644 --- a/Sources/cli-version/CliVersionCommand.swift +++ b/Sources/cli-version/Application.swift @@ -2,16 +2,16 @@ import ArgumentParser import Foundation @main -struct CliVersionCommand: AsyncParsableCommand { +struct Application: AsyncParsableCommand { static let configuration: CommandConfiguration = .init( - commandName: "cli-version", + commandName: "bump-version", version: VERSION ?? "0.0.0", subcommands: [ - Build.self, - Bump.self, - Generate.self, + BuildCommand.self, + BumpCommand.self, + GenerateCommand.self, UtilsCommand.self ], - defaultSubcommand: Bump.self + defaultSubcommand: BumpCommand.self ) } diff --git a/Sources/cli-version/BuildCommand.swift b/Sources/cli-version/BuildCommand.swift deleted file mode 100644 index f502e3f..0000000 --- a/Sources/cli-version/BuildCommand.swift +++ /dev/null @@ -1,19 +0,0 @@ -import ArgumentParser -import CliClient -import Foundation -import ShellClient - -extension CliVersionCommand { - struct Build: AsyncParsableCommand { - static let 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." - ) - - @OptionGroup var globals: GlobalOptions - - func run() async throws { - try await globals.shared().run(\.build) - } - } -} diff --git a/Sources/cli-version/BumpCommand.swift b/Sources/cli-version/BumpCommand.swift deleted file mode 100644 index 851972a..0000000 --- a/Sources/cli-version/BumpCommand.swift +++ /dev/null @@ -1,21 +0,0 @@ -import ArgumentParser -import CliClient -import Dependencies - -extension CliVersionCommand { - struct Bump: AsyncParsableCommand { - - static let configuration = CommandConfiguration( - commandName: "bump", - abstract: "Bump version of a command-line tool." - ) - - @OptionGroup var globals: GlobalOptions - - func run() async throws { - try await globals.shared().run(\.bump, args: nil) - } - } -} - -extension CliClient.BumpOption: EnumerableFlag {} diff --git a/Sources/cli-version/Commands/BuildCommand.swift b/Sources/cli-version/Commands/BuildCommand.swift new file mode 100644 index 0000000..811c8b1 --- /dev/null +++ b/Sources/cli-version/Commands/BuildCommand.swift @@ -0,0 +1,19 @@ +import ArgumentParser +import CliClient +import Foundation +import ShellClient + +struct BuildCommand: AsyncParsableCommand { + static let configuration: CommandConfiguration = .init( + commandName: "build", + abstract: "Used for the build with version plugin.", + discussion: "This should generally not be interacted with directly, outside of the build plugin.", + shouldDisplay: false + ) + + @OptionGroup var globals: GlobalOptions + + func run() async throws { + try await globals.run(\.build) + } +} diff --git a/Sources/cli-version/Commands/BumpCommand.swift b/Sources/cli-version/Commands/BumpCommand.swift new file mode 100644 index 0000000..50af2a5 --- /dev/null +++ b/Sources/cli-version/Commands/BumpCommand.swift @@ -0,0 +1,26 @@ +import ArgumentParser +import CliClient +import Dependencies + +struct BumpCommand: AsyncParsableCommand { + + static let configuration = CommandConfiguration( + commandName: "bump", + abstract: "Bump version of a command-line tool." + ) + + @OptionGroup var globals: GlobalOptions + + @Flag( + help: """ + The semvar bump option, this is ignored if the configuration is set to use a branch/commit sha strategy. + """ + ) + var bumpOption: CliClient.BumpOption = .patch + + func run() async throws { + try await globals.run(\.bump, args: bumpOption) + } +} + +extension CliClient.BumpOption: EnumerableFlag {} diff --git a/Sources/cli-version/Commands/GenerateCommand.swift b/Sources/cli-version/Commands/GenerateCommand.swift new file mode 100644 index 0000000..5099e2c --- /dev/null +++ b/Sources/cli-version/Commands/GenerateCommand.swift @@ -0,0 +1,19 @@ +import ArgumentParser +import CliClient +import Dependencies +import Foundation +import ShellClient + +struct GenerateCommand: AsyncParsableCommand { + static let configuration: CommandConfiguration = .init( + commandName: "generate", + 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." + ) + + @OptionGroup var globals: GlobalOptions + + func run() async throws { + try await globals.run(\.generate) + } +} diff --git a/Sources/cli-version/Commands/UtilsCommand.swift b/Sources/cli-version/Commands/UtilsCommand.swift new file mode 100644 index 0000000..54c2ffc --- /dev/null +++ b/Sources/cli-version/Commands/UtilsCommand.swift @@ -0,0 +1,95 @@ +import ArgumentParser +import ConfigurationClient +import CustomDump +import Dependencies +import FileClient +import Foundation + +struct UtilsCommand: AsyncParsableCommand { + static let configuration = CommandConfiguration( + commandName: "utils", + abstract: "Utility commands", + subcommands: [ + DumpConfig.self, + GenerateConfig.self + ] + ) +} + +extension UtilsCommand { + struct DumpConfig: AsyncParsableCommand { + static let configuration = CommandConfiguration( + commandName: "dump-config", + abstract: "Show the parsed configuration.", + aliases: ["dc"] + ) + + @OptionGroup var globals: GlobalOptions + + func run() async throws { + let configuration = try await globals.runClient(\.parsedConfiguration) + customDump(configuration) + } + } + + struct GenerateConfig: AsyncParsableCommand { + static let configuration: CommandConfiguration = .init( + commandName: "generate-config", + abstract: "Generate a configuration file.", + aliases: ["gc"] + ) + + @OptionGroup var configOptions: ConfigurationOptions + + @Flag( + help: "The style of the configuration." + ) + var style: Style = .semvar + + @Argument( + help: """ + Arguments / options used for custom pre-release, options / flags must proceed a '--' in + the command. These are ignored if the `--custom` flag is not set. + """ + ) + var extraOptions: [String] = [] + + func run() async throws { + try await withSetupDependencies { + @Dependency(\.configurationClient) var configurationClient + + let strategy: Configuration.VersionStrategy + + switch style { + case .branch: + strategy = .branch(includeCommitSha: configOptions.commitSha) + case .semvar: + strategy = try .semvar(configOptions.semvarOptions(extraOptions: extraOptions)) + } + + let configuration = try Configuration( + target: configOptions.target(), + strategy: strategy + ) + + let url: URL + switch configOptions.configurationFile { + case let .some(path): + url = URL(filePath: path) + case .none: + url = URL(filePath: ".bump-version.json") + } + + try await configurationClient.write(configuration, url) + + print(url.cleanFilePath) + } + } + } +} + +extension UtilsCommand.GenerateConfig { + enum Style: EnumerableFlag { + case branch, semvar + } +} diff --git a/Sources/cli-version/ConfigurationExtensions.swift b/Sources/cli-version/ConfigurationExtensions.swift deleted file mode 100644 index 788740e..0000000 --- a/Sources/cli-version/ConfigurationExtensions.swift +++ /dev/null @@ -1,61 +0,0 @@ -// import ConfigurationClient -// import Dependencies -// import FileClient -// import Foundation -// -// extension Configuration { -// -// func mergingTarget(_ otherTarget: Configuration.Target?) -> Self { -// .init( -// target: otherTarget ?? target, -// strategy: strategy -// ) -// } -// -// func mergingStrategy(_ otherStrategy: Configuration.VersionStrategy?) -> Self { -// .init( -// target: target, -// strategy: strategy?.merging(otherStrategy) -// ) -// } -// } -// -// extension Configuration.Branch { -// func merging(_ other: Self?) -> Self { -// return .init(includeCommitSha: other?.includeCommitSha ?? includeCommitSha) -// } -// } -// -// extension Configuration.SemVar { -// func merging(_ other: Self?) -> Self { -// .init( -// preRelease: other?.preRelease ?? preRelease, -// requireExistingFile: other?.requireExistingFile ?? requireExistingFile, -// requireExistingSemVar: other?.requireExistingSemVar ?? requireExistingSemVar -// ) -// } -// } -// -// extension Configuration.VersionStrategy { -// func merging(_ other: Self?) -> Self { -// guard let branch else { -// guard let semvar else { return self } -// return .semvar(semvar.merging(other?.semvar)) -// } -// return .branch(branch.merging(other?.branch)) -// } -// } -// -// @discardableResult -// func withConfiguration( -// path: String?, -// _ operation: (Configuration) async throws -> T -// ) async throws -> T { -// @Dependency(\.configurationClient) var configurationClient -// -// let configuration = try await configurationClient.findAndLoad( -// path != nil ? URL(filePath: path!) : nil -// ) -// -// return try await operation(configuration) -// } diff --git a/Sources/cli-version/Documentation.docc/Application.md b/Sources/cli-version/Documentation.docc/Application.md new file mode 100644 index 0000000..db8e1b6 --- /dev/null +++ b/Sources/cli-version/Documentation.docc/Application.md @@ -0,0 +1,5 @@ +# Application + +## Articles + +- diff --git a/Sources/cli-version/Documentation.docc/Articles/Installation.md b/Sources/cli-version/Documentation.docc/Articles/Installation.md new file mode 100644 index 0000000..a123882 --- /dev/null +++ b/Sources/cli-version/Documentation.docc/Articles/Installation.md @@ -0,0 +1,3 @@ +# Installation + +You can install the command-line application by... diff --git a/Sources/cli-version/GenerateCommand.swift b/Sources/cli-version/GenerateCommand.swift deleted file mode 100644 index b99b856..0000000 --- a/Sources/cli-version/GenerateCommand.swift +++ /dev/null @@ -1,20 +0,0 @@ -import ArgumentParser -import CliClient -import Dependencies -import Foundation -import ShellClient - -extension CliVersionCommand { - struct Generate: AsyncParsableCommand { - static let 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." - ) - - @OptionGroup var globals: GlobalOptions - - func run() async throws { - try await globals.shared().run(\.generate) - } - } -} diff --git a/Sources/cli-version/GlobalOptions.swift b/Sources/cli-version/GlobalOptions.swift index 3b1d9e2..01260eb 100644 --- a/Sources/cli-version/GlobalOptions.swift +++ b/Sources/cli-version/GlobalOptions.swift @@ -7,24 +7,8 @@ import Rainbow struct GlobalOptions: ParsableArguments { - @Option( - name: .shortAndLong, - help: "Specify the path to a configuration file." - ) - var configurationFile: String? - - @OptionGroup var targetOptions: TargetOptions - - @OptionGroup var semvarOptions: SemVarOptions - - @Flag( - name: .long, - inversion: .prefixedNo, - help: """ - Include the short commit sha in version or pre-release branch style output. - """ - ) - var commitSha: Bool = true + @OptionGroup + var configOptions: ConfigurationOptions @Option( name: .customLong("git-directory"), @@ -44,6 +28,37 @@ struct GlobalOptions: ParsableArguments { ) var verbose: Int + @Argument( + help: """ + Arguments / options used for custom pre-release, options / flags must proceed a '--' in + the command. These are ignored if the `--custom` flag is not set. + """ + ) + var extraOptions: [String] = [] + +} + +struct ConfigurationOptions: ParsableArguments { + @Option( + name: .shortAndLong, + help: "Specify the path to a configuration file.", + completion: .file(extensions: ["json"]) + ) + var configurationFile: String? + + @OptionGroup var targetOptions: TargetOptions + + @OptionGroup var semvarOptions: SemVarOptions + + @Flag( + name: .long, + inversion: .prefixedNo, + help: """ + Include the short commit sha in version or pre-release branch style output. + """ + ) + var commitSha: Bool = true + } struct TargetOptions: ParsableArguments { @@ -67,8 +82,6 @@ struct TargetOptions: ParsableArguments { } -// TODO: Need to be able to pass in arguments for custom command pre-release option. - struct PreReleaseOptions: ParsableArguments { @Flag( @@ -101,14 +114,13 @@ struct PreReleaseOptions: ParsableArguments { ) var preReleasePrefix: String? - @Option( + @Flag( name: .long, help: """ - Apply custom pre-release suffix, can also use branch or tag along with this - option as a prefix, used if branch is not set. (example: \"rc\") + Apply custom pre-release suffix, using extra options / arguments passed in after a '--'. """ ) - var custom: String? + var customPreRelease: Bool = false } @@ -130,102 +142,3 @@ struct SemVarOptions: ParsableArguments { @OptionGroup var preRelease: PreReleaseOptions } - -// TODO: Move these to global options. -extension CliClient.SharedOptions { - - func run(_ keyPath: KeyPath String>) async throws { - try await withDependencies { - $0.fileClient = .liveValue - $0.gitClient = .liveValue - $0.cliClient = .liveValue - } operation: { - @Dependency(\.cliClient) var cliClient - let output = try await cliClient[keyPath: keyPath](self) - print(output) - } - } - - func run( - _ keyPath: KeyPath String>, - args: T - ) async throws { - try await withDependencies { - $0.fileClient = .liveValue - $0.gitClient = .liveValue - $0.cliClient = .liveValue - } operation: { - @Dependency(\.cliClient) var cliClient - let output = try await cliClient[keyPath: keyPath](args, self) - print(output) - } - } -} - -extension GlobalOptions { - - func shared() throws -> CliClient.SharedOptions { - try .init( - allowPreReleaseTag: !semvarOptions.preRelease.disablePreRelease, - dryRun: dryRun, - gitDirectory: gitDirectory, - verbose: verbose, - target: targetOptions.configTarget(), - branch: .init(includeCommitSha: commitSha), - semvar: semvarOptions.configSemVarOptions(), - configurationFile: configurationFile - ) - } - -} - -// MARK: - Helpers - -private extension TargetOptions { - func configTarget() throws -> Configuration.Target? { - guard let path else { - guard let module else { - return nil - } - return .init(module: .init(module, fileName: fileName)) - } - return .init(path: path) - } -} - -extension PreReleaseOptions { - - // FIX: - func configPreReleaseStrategy() throws -> Configuration.PreRelease? { - return nil - // guard let custom else { - // if useBranchAsPreRelease { - // return .branch() - // } else if useTagAsPreRelease { - // return .gitTag - // } else { - // return nil - // } - // } - // - // if useBranchAsPreRelease { - // return .customBranchPrefix(custom) - // } else if useTagAsPreRelease { - // return .customGitTagPrefix(custom) - // } else { - // return .custom(custom) - // } - } -} - -extension SemVarOptions { - - func configSemVarOptions() throws -> Configuration.SemVar { - try .init( - preRelease: preRelease.configPreReleaseStrategy(), - requireExistingFile: requireExistingFile, - requireExistingSemVar: requireExistingSemvar - ) - } - -} diff --git a/Sources/cli-version/Helpers/GlobalOptions+run.swift b/Sources/cli-version/Helpers/GlobalOptions+run.swift new file mode 100644 index 0000000..1eef904 --- /dev/null +++ b/Sources/cli-version/Helpers/GlobalOptions+run.swift @@ -0,0 +1,123 @@ +import CliClient +import ConfigurationClient +import Dependencies +import FileClient +import GitClient + +@discardableResult +func withSetupDependencies( + _ operation: () async throws -> T +) async throws -> T { + try await withDependencies { + $0.fileClient = .liveValue + $0.gitClient = .liveValue + $0.cliClient = .liveValue + $0.configurationClient = .liveValue + } operation: { + try await operation() + } +} + +extension GlobalOptions { + + func runClient( + _ keyPath: KeyPath T> + ) async throws -> T { + try await withSetupDependencies { + @Dependency(\.cliClient) var cliClient + return try await cliClient[keyPath: keyPath](shared()) + } + } + + func runClient( + _ keyPath: KeyPath T>, + args: A + ) async throws -> T { + try await withSetupDependencies { + @Dependency(\.cliClient) var cliClient + return try await cliClient[keyPath: keyPath](args, shared()) + } + } + + func run( + _ keyPath: KeyPath String> + ) async throws { + let output = try await runClient(keyPath) + print(output) + } + + func run( + _ keyPath: KeyPath String>, + args: T + ) async throws { + let output = try await runClient(keyPath, args: args) + print(output) + } + + func shared() throws -> CliClient.SharedOptions { + try .init( + allowPreReleaseTag: !configOptions.semvarOptions.preRelease.disablePreRelease, + dryRun: dryRun, + gitDirectory: gitDirectory, + verbose: verbose, + target: configOptions.target(), + branch: .init(includeCommitSha: configOptions.commitSha), + semvar: configOptions.semvarOptions(extraOptions: extraOptions), + configurationFile: configOptions.configurationFile + ) + } + +} + +private extension TargetOptions { + func configTarget() throws -> Configuration.Target? { + guard let path else { + guard let module else { + return nil + } + return .init(module: .init(module, fileName: fileName)) + } + return .init(path: path) + } +} + +extension PreReleaseOptions { + + func configPreReleaseStrategy(includeCommitSha: Bool, extraOptions: [String]) throws -> Configuration.PreRelease? { + if useBranchAsPreRelease { + return .init(prefix: preReleasePrefix, strategy: .branch(includeCommitSha: includeCommitSha)) + } else if useTagAsPreRelease { + return .init(prefix: preReleasePrefix, strategy: .gitTag) + } else if customPreRelease { + guard extraOptions.count > 0 else { + throw ExtraOptionsEmpty() + } + return .init(prefix: preReleasePrefix, strategy: .command(arguments: extraOptions)) + } + return nil + } +} + +extension SemVarOptions { + + func configSemVarOptions(includeCommitSha: Bool, extraOptions: [String]) throws -> Configuration.SemVar { + try .init( + preRelease: preRelease.configPreReleaseStrategy(includeCommitSha: includeCommitSha, extraOptions: extraOptions), + requireExistingFile: requireExistingFile, + requireExistingSemVar: requireExistingSemvar + ) + } +} + +extension ConfigurationOptions { + + func target() throws -> Configuration.Target? { + try targetOptions.configTarget() + } + + func semvarOptions(extraOptions: [String]) throws -> Configuration.SemVar { + try semvarOptions.configSemVarOptions(includeCommitSha: commitSha, extraOptions: extraOptions) + } +} + +struct ExtraOptionsEmpty: Error {} diff --git a/Sources/cli-version/UtilsCommand.swift b/Sources/cli-version/UtilsCommand.swift deleted file mode 100644 index 66ae26b..0000000 --- a/Sources/cli-version/UtilsCommand.swift +++ /dev/null @@ -1,48 +0,0 @@ -import ArgumentParser -import ConfigurationClient -import CustomDump -import Dependencies -import FileClient -import Foundation - -struct UtilsCommand: AsyncParsableCommand { - static let configuration = CommandConfiguration( - commandName: "utils", - abstract: "Utility commands", - subcommands: [ - DumpConfig.self - ] - ) -} - -extension UtilsCommand { - struct DumpConfig: AsyncParsableCommand { - static let configuration = CommandConfiguration( - commandName: "dump-config", - abstract: "Show the parsed configuration." - ) - - @Argument( - help: """ - Optional path to the configuration file, if not supplied will search the current directory - """, - completion: .file(extensions: ["toml", "json"]) - ) - var file: String? - - func run() async throws { - try await withDependencies { - $0.fileClient = .liveValue - $0.configurationClient = .liveValue - } operation: { - @Dependency(\.configurationClient) var configurationClient - - let configuration = try await configurationClient.findAndLoad( - file != nil ? URL(filePath: file!) : nil - ) - - customDump(configuration) - } - } - } -} diff --git a/Tests/CliVersionTests/CliClientTests.swift b/Tests/CliVersionTests/CliClientTests.swift index 0b293d2..d599827 100644 --- a/Tests/CliVersionTests/CliClientTests.swift +++ b/Tests/CliVersionTests/CliClientTests.swift @@ -97,6 +97,8 @@ struct CliClientTests { $0.fileClient.fileExists = { _ in false } $0.gitClient = .mock(.tag("1.0.0")) $0.cliClient = .liveValue + $0.configurationClient = .liveValue + $0.configurationClient.find = { _ in URL(filePath: "/") } setupDependencies(&$0) } operation: { try await operation() @@ -140,10 +142,6 @@ extension CliClient.SharedOptions { target: .init(module: .init(target)), branch: versionStrategy.branch, semvar: versionStrategy.semvar - // configuration: .init( - // target: .init(module: .init(target)), - // strategy: versionStrategy - // ) ) } } diff --git a/Tests/ConfigurationClientTests/ConfigurationClientTests.swift b/Tests/ConfigurationClientTests/ConfigurationClientTests.swift index ed3bb16..d220e43 100644 --- a/Tests/ConfigurationClientTests/ConfigurationClientTests.swift +++ b/Tests/ConfigurationClientTests/ConfigurationClientTests.swift @@ -29,7 +29,7 @@ struct ConfigurationClientTests { for ext in ["json"] { let fileUrl = url.appending(path: "test.\(ext)") - let configuration = Configuration.mock + let configuration = Configuration.mock() try await configurationClient.write(configuration, fileUrl) let loaded = try await configurationClient.load(fileUrl) @@ -62,7 +62,7 @@ struct ConfigurationClientTests { for ext in ["json"] { let fileUrl = url.appending(path: ".bump-version.\(ext)") - let configuration = Configuration.mock + let configuration = Configuration.mock() try await configurationClient.write(configuration, fileUrl) let loaded = try await configurationClient.findAndLoad(fileUrl) @@ -74,56 +74,6 @@ struct ConfigurationClientTests { } } - // @Test - // func writeDefault() async throws { - // try await run { - // @Dependency(\.coders) var coders - // @Dependency(\.configurationClient) var configurationClient - // - // // let configuration = Configuration.customPreRelease - // // try await configurationClient.write(configuration, .json(URL(filePath: ".bump-version.json"))) - // - // // let target = Configuration.Target2.path("foo") - // // let target = Configuration.Target2.gitTag - // // let target = Configuration.Target2.branch() - // let target = Configuration2.mock - // - // let encoded = try coders.jsonEncoder().encode(target) - // let url = URL(filePath: ".bump-version.json") - // try encoded.write(to: url) - // - // let data = try Data(contentsOf: url) - // let decoded = try coders.jsonDecoder().decode(Configuration2.self, from: data) - // print(decoded) - // } - // } - - // @Test - // func tomlPlayground() throws { - // let jsonEncoder = JSONEncoder() - // let encoder = TOMLEncoder() - // let decoder = TOMLDecoder() - // - // enum TestType: Codable { - // case one - // case hello(Hello) - // - // struct Hello: Codable { - // let value: String - // } - // } - // - // struct TestContainer: Codable { - // let testType: TestType - // } - // - // let sut = TestContainer(testType: .hello(.init(value: "world"))) - // let encoded = try encoder.encode(sut) - // print(encoded) - // // let decoded = try decoder.decode(TestContainer.self, from: encoded) - // // #expect(decoded.testType == sut.testType) - // } - func run( setupDependencies: @escaping (inout DependencyValues) -> Void = { _ in }, operation: () async throws -> Void diff --git a/Tests/GitClientTests/GitClientTests.swift b/Tests/GitClientTests/GitClientTests.swift new file mode 100644 index 0000000..97fdeb1 --- /dev/null +++ b/Tests/GitClientTests/GitClientTests.swift @@ -0,0 +1,59 @@ +import Dependencies +import GitClient +import ShellClient +import Testing + +@Suite("GitClientTests") +struct GitClientTests { + + @Test(arguments: GitClientVersionTestArgument.testCases) + func testGitClient(input: GitClientVersionTestArgument) async throws { + let arguments = try await run { + @Dependency(\.gitClient) var gitClient + _ = try await gitClient.version(.init(style: input.style)) + } + #expect(arguments == input.expected) + } + + func run( + _ operation: () async throws -> Void + ) async throws -> [[String]] { + let captured = CapturedCommand() + + try await withDependencies { + $0.asyncShellClient = .capturing(captured) + $0.fileClient = .noop + $0.gitClient = .liveValue + } operation: { + try await operation() + } + return await captured.commands.map(\.arguments) + } +} + +struct GitClientVersionTestArgument { + let style: GitClient.CurrentVersionOption.Style + let expected: [[String]] + + static let testCases: [Self] = [ + .init( + style: .tag(exactMatch: true), + expected: [["git", "describe", "--tags", "--exact-match"]] + ), + .init( + style: .tag(exactMatch: false), + expected: [["git", "describe", "--tags"]] + ), + .init( + style: .branch(commitSha: false), + expected: [["git", "symbolic-ref", "--quiet", "--short", "HEAD"]] + ), + .init( + style: .branch(commitSha: true), + expected: [ + ["git", "rev-parse", "--short", "HEAD"], + ["git", "symbolic-ref", "--quiet", "--short", "HEAD"] + ] + ) + ] +}