diff --git a/.bump-version.json b/.bump-version.json index 3de4041..776c3dd 100644 --- a/.bump-version.json +++ b/.bump-version.json @@ -6,7 +6,6 @@ }, "target" : { "module" : { - "fileName" : "Version.swift", "name" : "cli-version" } } diff --git a/Package.swift b/Package.swift index 8b0166a..f46c6c8 100644 --- a/Package.swift +++ b/Package.swift @@ -9,6 +9,9 @@ let package = Package( ], products: [ .library(name: "CliClient", targets: ["CliClient"]), + .library(name: "ConfigurationClient", targets: ["ConfigurationClient"]), + .library(name: "FileClient", targets: ["FileClient"]), + .library(name: "GitClient", targets: ["GitClient"]), .plugin(name: "BuildWithVersionPlugin", targets: ["BuildWithVersionPlugin"]), .plugin(name: "GenerateVersionPlugin", targets: ["GenerateVersionPlugin"]), .plugin(name: "UpdateVersionPlugin", targets: ["UpdateVersionPlugin"]) diff --git a/Sources/CliClient/CliClient.swift b/Sources/CliClient/CliClient.swift index b167178..236899e 100644 --- a/Sources/CliClient/CliClient.swift +++ b/Sources/CliClient/CliClient.swift @@ -6,8 +6,6 @@ import Foundation import GitClient import ShellClient -// TODO: Integrate ConfigurationClient - public extension DependencyValues { var cliClient: CliClient { @@ -35,26 +33,55 @@ public struct CliClient: Sendable { public struct SharedOptions: Equatable, Sendable { + let allowPreReleaseTag: Bool let dryRun: Bool let gitDirectory: String? let logLevel: Logger.Level - let configuration: Configuration + let target: Configuration.Target? + let branch: Configuration.Branch? + let semvar: Configuration.SemVar? + let configurationFile: String? public init( + allowPreReleaseTag: Bool = true, dryRun: Bool = false, gitDirectory: String? = nil, logLevel: Logger.Level = .debug, - configuration: Configuration + target: Configuration.Target? = nil, + branch: Configuration.Branch? = nil, + semvar: Configuration.SemVar? = nil, + configurationFile: String? = nil ) { + self.allowPreReleaseTag = allowPreReleaseTag self.dryRun = dryRun self.gitDirectory = gitDirectory self.logLevel = logLevel - self.configuration = configuration + self.target = target + self.branch = branch + self.semvar = semvar + self.configurationFile = configurationFile } - var allowPreReleaseTag: Bool { - guard let semvar = configuration.strategy?.semvar else { return false } - return semvar.preRelease != nil + public init( + allowPreReleaseTag: Bool = true, + dryRun: Bool = false, + gitDirectory: String? = nil, + verbose: Int, + target: Configuration.Target? = nil, + branch: Configuration.Branch? = nil, + semvar: Configuration.SemVar? = nil, + configurationFile: String? = nil + ) { + self.init( + allowPreReleaseTag: allowPreReleaseTag, + dryRun: dryRun, + gitDirectory: gitDirectory, + logLevel: .init(verbose: verbose), + target: target, + branch: branch, + semvar: semvar, + configurationFile: configurationFile + ) } } diff --git a/Sources/CliClient/Internal/Configuration+merging.swift b/Sources/CliClient/Internal/Configuration+merging.swift new file mode 100644 index 0000000..5b66dac --- /dev/null +++ b/Sources/CliClient/Internal/Configuration+merging.swift @@ -0,0 +1,70 @@ +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)) + } +} + +extension Configuration { + func merging(_ other: Self?) -> Self { + var output = self + output = output.mergingTarget(other?.target) + output = output.mergingStrategy(other?.strategy) + return output + } +} + +@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/CliClient/Internal/Internal.swift b/Sources/CliClient/Internal/Internal.swift index a88f330..1689405 100644 --- a/Sources/CliClient/Internal/Internal.swift +++ b/Sources/CliClient/Internal/Internal.swift @@ -1,3 +1,4 @@ +import ConfigurationClient import Dependencies import FileClient import Foundation @@ -13,19 +14,37 @@ public extension CliClient.SharedOptions { try await withDependencies { $0.logger.logLevel = logLevel } operation: { - @Dependency(\.logger) var logger + // Load the default configuration, if it exists. + try await withConfiguration(path: configurationFile) { configuration in + @Dependency(\.logger) var logger - let targetUrl = try configuration.targetUrl(gitDirectory: gitDirectory) - logger.debug("Target: \(targetUrl.cleanFilePath)") + // Merge any configuration set from caller into default configuration. + var configuration = configuration + configuration = configuration.mergingTarget(target) - try await operation( - configuration.currentVersion( - targetUrl: targetUrl, - gitDirectory: gitDirectory + 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. + let targetUrl = try configuration.targetUrl(gitDirectory: gitDirectory) + logger.debug("Target: \(targetUrl.cleanFilePath)") + + // Perform the operation, which generates the new version and writes it. + try await operation( + configuration.currentVersion( + targetUrl: targetUrl, + gitDirectory: gitDirectory + ) ) - ) - return targetUrl.cleanFilePath + // Return the file path we wrote the version to. + return targetUrl.cleanFilePath + } } } diff --git a/Sources/CliClient/Internal/VersionStrategy+currentVersion.swift b/Sources/CliClient/Internal/VersionStrategy+currentVersion.swift deleted file mode 100644 index 78290ea..0000000 --- a/Sources/CliClient/Internal/VersionStrategy+currentVersion.swift +++ /dev/null @@ -1,104 +0,0 @@ -// import ConfigurationClient -// import Dependencies -// import FileClient -// import struct Foundation.URL -// import GitClient -// -// @_spi(Internal) -// public extension CliClient.PreReleaseStrategy { -// -// func preReleaseString(gitDirectory: String?) async throws -> String { -// @Dependency(\.gitClient) var gitClient -// switch self { -// case let .custom(string, child): -// guard let child else { return string } -// return try await "\(string)-\(child.preReleaseString(gitDirectory: gitDirectory))" -// case .tag: -// return try await gitClient.version(.init( -// gitDirectory: gitDirectory, -// style: .tag(exactMatch: false) -// )).description -// case .branchAndCommit: -// return try await gitClient.version(.init( -// gitDirectory: gitDirectory, -// style: .branch(commitSha: true) -// )).description -// } -// } -// } -// -// @_spi(Internal) -// public extension CliClient.VersionStrategy.SemVarOptions { -// -// private func applyingPreRelease(_ semVar: SemVar, _ gitDirectory: String?) async throws -> SemVar { -// guard let preReleaseStrategy else { return semVar } -// let preRelease = try await preReleaseStrategy.preReleaseString(gitDirectory: gitDirectory) -// return semVar.applyingPreRelease(preRelease) -// } -// -// func currentVersion(file: URL, gitDirectory: String? = nil) async throws -> CurrentVersionContainer.Version { -// @Dependency(\.fileClient) var fileClient -// @Dependency(\.gitClient) var gitClient -// -// let fileOutput = try? await fileClient.semVar(file: file, gitDirectory: gitDirectory) -// var semVar = fileOutput?.semVar -// let usesOptionalType = fileOutput?.usesOptionalType -// -// if requireExistingFile { -// guard let semVar else { -// throw CliClientError.fileDoesNotExist(path: file.cleanFilePath) -// } -// return try await .semVar( -// applyingPreRelease(semVar, gitDirectory), -// usesOptionalType: usesOptionalType ?? false -// ) -// } -// -// // Didn't have existing semVar loaded from file, so check for git-tag. -// -// semVar = try await gitClient.version(.init( -// gitDirectory: gitDirectory, -// style: .tag(exactMatch: false) -// )).semVar -// if requireExistingSemVar { -// guard let semVar else { -// fatalError() -// } -// return try await .semVar( -// applyingPreRelease(semVar, gitDirectory), -// usesOptionalType: usesOptionalType ?? false -// ) -// } -// -// return try await .semVar( -// applyingPreRelease(.init(), gitDirectory), -// usesOptionalType: usesOptionalType ?? false -// ) -// } -// } -// -// @_spi(Internal) -// public extension CliClient.VersionStrategy { -// -// func currentVersion(file: URL, gitDirectory: String?) async throws -> CurrentVersionContainer { -// @Dependency(\.gitClient) var gitClient -// -// switch self { -// case .branchAndCommit: -// return try await .init( -// targetUrl: file, -// version: .string( -// gitClient.version(.init( -// gitDirectory: gitDirectory, -// style: .branch(commitSha: true) -// )).description -// ) -// ) -// case let .semVar(options): -// return try await .init( -// targetUrl: file, -// version: options.currentVersion(file: file, gitDirectory: gitDirectory) -// ) -// } -// } -// } diff --git a/Sources/cli-version/BuildCommand.swift b/Sources/cli-version/BuildCommand.swift index e623468..f502e3f 100644 --- a/Sources/cli-version/BuildCommand.swift +++ b/Sources/cli-version/BuildCommand.swift @@ -7,38 +7,10 @@ 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.", - subcommands: [BranchStyle.self, SemVarStyle.self], - defaultSubcommand: SemVarStyle.self - ) - } -} - -extension CliVersionCommand.Build { - struct BranchStyle: AsyncParsableCommand { - - static let configuration: CommandConfiguration = .init( - commandName: "branch", - abstract: "Build using branch and commit sha as the version.", - discussion: "This should generally not be interacted with directly, outside of the plugin usage context." + discussion: "This should generally not be interacted with directly, outside of the build plugin." ) - @OptionGroup var globals: GlobalBranchOptions - - func run() async throws { - try await globals.shared().run(\.build) - } - } - - struct SemVarStyle: AsyncParsableCommand { - - static let configuration: CommandConfiguration = .init( - commandName: "semvar", - abstract: "Generates a version file with SemVar style.", - discussion: "This should generally not be interacted with directly, outside of the plugin usage context." - ) - - @OptionGroup var globals: GlobalSemVarOptions + @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 index 0dab55f..851972a 100644 --- a/Sources/cli-version/BumpCommand.swift +++ b/Sources/cli-version/BumpCommand.swift @@ -7,49 +7,15 @@ extension CliVersionCommand { static let configuration = CommandConfiguration( commandName: "bump", - abstract: "Bump version of a command-line tool.", - subcommands: [ - SemVarStyle.self, - BranchStyle.self - ], - defaultSubcommand: SemVarStyle.self - ) - } -} - -extension CliVersionCommand.Bump { - - struct BranchStyle: AsyncParsableCommand { - - static let configuration = CommandConfiguration( - commandName: "branch", - abstract: "Bump using the current branch and commit sha." + abstract: "Bump version of a command-line tool." ) - @OptionGroup var globals: GlobalBranchOptions + @OptionGroup var globals: GlobalOptions func run() async throws { try await globals.shared().run(\.bump, args: nil) } } - - struct SemVarStyle: AsyncParsableCommand { - - static let configuration = CommandConfiguration( - commandName: "semvar", - abstract: "Bump using semvar style options." - ) - - @OptionGroup var globals: GlobalSemVarOptions - - @Flag - var bumpOption: CliClient.BumpOption = .patch - - func run() async throws { - try await globals.shared().run(\.bump, args: bumpOption) - } - - } } extension CliClient.BumpOption: EnumerableFlag {} diff --git a/Sources/cli-version/ConfigurationExtensions.swift b/Sources/cli-version/ConfigurationExtensions.swift index e01e6ed..788740e 100644 --- a/Sources/cli-version/ConfigurationExtensions.swift +++ b/Sources/cli-version/ConfigurationExtensions.swift @@ -1,61 +1,61 @@ -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) -} +// 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/GenerateCommand.swift b/Sources/cli-version/GenerateCommand.swift index 197d7ca..b99b856 100644 --- a/Sources/cli-version/GenerateCommand.swift +++ b/Sources/cli-version/GenerateCommand.swift @@ -8,45 +8,13 @@ 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.", - subcommands: [BranchStyle.self, SemVarStyle.self], - defaultSubcommand: SemVarStyle.self - ) - } -} - -extension CliVersionCommand.Generate { - struct BranchStyle: AsyncParsableCommand { - - static let configuration: CommandConfiguration = .init( - commandName: "branch", - abstract: "Generates a version file with branch and commit sha as the version.", discussion: "This command can be interacted with directly, outside of the plugin usage context." ) - @OptionGroup var globals: GlobalBranchOptions - - func run() async throws { - try await globals.shared().run(\.generate) - } - } - - struct SemVarStyle: AsyncParsableCommand { - - static let configuration: CommandConfiguration = .init( - commandName: "semvar", - abstract: "Generates a version file with SemVar style.", - discussion: "This command can be interacted with directly, outside of the plugin usage context." - ) - - @OptionGroup var globals: GlobalSemVarOptions + @OptionGroup var globals: GlobalOptions func run() async throws { try await globals.shared().run(\.generate) } } } - -private enum GenerationError: Error { - case fileExists(path: String) -} diff --git a/Sources/cli-version/GlobalOptions.swift b/Sources/cli-version/GlobalOptions.swift index f39c942..3b1d9e2 100644 --- a/Sources/cli-version/GlobalOptions.swift +++ b/Sources/cli-version/GlobalOptions.swift @@ -5,7 +5,7 @@ import Dependencies import Foundation import Rainbow -struct GlobalOptions: ParsableArguments { +struct GlobalOptions: ParsableArguments { @Option( name: .shortAndLong, @@ -14,7 +14,17 @@ struct GlobalOptions: ParsableArguments { var configurationFile: String? @OptionGroup var targetOptions: TargetOptions - @OptionGroup var child: Child + + @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 @Option( name: .customLong("git-directory"), @@ -36,8 +46,6 @@ struct GlobalOptions: ParsableArguments { } -struct Empty: ParsableArguments {} - struct TargetOptions: ParsableArguments { @Option( name: .shortAndLong, @@ -59,7 +67,16 @@ struct TargetOptions: ParsableArguments { } +// TODO: Need to be able to pass in arguments for custom command pre-release option. + struct PreReleaseOptions: ParsableArguments { + + @Flag( + name: .shortAndLong, + help: "" + ) + var disablePreRelease: Bool = false + @Flag( name: [.customShort("s"), .customLong("pre-release-branch-style")], help: """ @@ -76,6 +93,14 @@ struct PreReleaseOptions: ParsableArguments { ) var useTagAsPreRelease: Bool = false + @Option( + name: .long, + help: """ + Add / use a pre-release prefix string. + """ + ) + var preReleasePrefix: String? + @Option( name: .long, help: """ @@ -84,6 +109,7 @@ struct PreReleaseOptions: ParsableArguments { """ ) var custom: String? + } struct SemVarOptions: ParsableArguments { @@ -105,25 +131,7 @@ struct SemVarOptions: ParsableArguments { @OptionGroup var preRelease: PreReleaseOptions } -typealias GlobalSemVarOptions = GlobalOptions -typealias GlobalBranchOptions = GlobalOptions - -extension GlobalSemVarOptions { - func shared() async throws -> CliClient.SharedOptions { - try await withConfiguration(path: configurationFile) { configuration in - try shared(configuration.mergingStrategy(.semvar(child.configSemVarOptions()))) - } - } -} - -extension GlobalBranchOptions { - func shared() async throws -> CliClient.SharedOptions { - try await withConfiguration(path: configurationFile) { configuration in - try shared(configuration.mergingStrategy(.branch())) - } - } -} - +// TODO: Move these to global options. extension CliClient.SharedOptions { func run(_ keyPath: KeyPath String>) async throws { @@ -156,12 +164,16 @@ extension CliClient.SharedOptions { extension GlobalOptions { - func shared(_ configuration: Configuration) throws -> CliClient.SharedOptions { - .init( + func shared() throws -> CliClient.SharedOptions { + try .init( + allowPreReleaseTag: !semvarOptions.preRelease.disablePreRelease, dryRun: dryRun, gitDirectory: gitDirectory, - logLevel: .init(verbose: verbose), - configuration: configuration + verbose: verbose, + target: targetOptions.configTarget(), + branch: .init(includeCommitSha: commitSha), + semvar: semvarOptions.configSemVarOptions(), + configurationFile: configurationFile ) } @@ -170,20 +182,6 @@ extension GlobalOptions { // MARK: - Helpers private extension TargetOptions { - func target() throws -> String { - guard let path else { - guard let module else { - print("Neither target path or module was set.") - throw InvalidTargetOption() - } - - return "\(module)/\(fileName)" - } - return path - } - - struct InvalidTargetOption: Error {} - func configTarget() throws -> Configuration.Target? { guard let path else { guard let module else { diff --git a/Tests/CliVersionTests/CliClientTests.swift b/Tests/CliVersionTests/CliClientTests.swift index d052651..0b293d2 100644 --- a/Tests/CliVersionTests/CliClientTests.swift +++ b/Tests/CliVersionTests/CliClientTests.swift @@ -133,14 +133,17 @@ extension CliClient.SharedOptions { logLevel: Logger.Level = .trace, versionStrategy: Configuration.VersionStrategy = .semvar(.init()) ) -> Self { - .init( + return .init( dryRun: dryRun, gitDirectory: gitDirectory, logLevel: logLevel, - configuration: .init( - target: .init(module: .init(target)), - strategy: versionStrategy - ) + target: .init(module: .init(target)), + branch: versionStrategy.branch, + semvar: versionStrategy.semvar + // configuration: .init( + // target: .init(module: .init(target)), + // strategy: versionStrategy + // ) ) } }