feat: Updating command options.

This commit is contained in:
2024-12-24 10:39:56 -05:00
parent f2a2374c4f
commit 8aa4b73cab
12 changed files with 249 additions and 328 deletions

View File

@@ -6,7 +6,6 @@
}, },
"target" : { "target" : {
"module" : { "module" : {
"fileName" : "Version.swift",
"name" : "cli-version" "name" : "cli-version"
} }
} }

View File

@@ -9,6 +9,9 @@ let package = Package(
], ],
products: [ products: [
.library(name: "CliClient", targets: ["CliClient"]), .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: "BuildWithVersionPlugin", targets: ["BuildWithVersionPlugin"]),
.plugin(name: "GenerateVersionPlugin", targets: ["GenerateVersionPlugin"]), .plugin(name: "GenerateVersionPlugin", targets: ["GenerateVersionPlugin"]),
.plugin(name: "UpdateVersionPlugin", targets: ["UpdateVersionPlugin"]) .plugin(name: "UpdateVersionPlugin", targets: ["UpdateVersionPlugin"])

View File

@@ -6,8 +6,6 @@ import Foundation
import GitClient import GitClient
import ShellClient import ShellClient
// TODO: Integrate ConfigurationClient
public extension DependencyValues { public extension DependencyValues {
var cliClient: CliClient { var cliClient: CliClient {
@@ -35,26 +33,55 @@ public struct CliClient: Sendable {
public struct SharedOptions: Equatable, Sendable { public struct SharedOptions: Equatable, Sendable {
let allowPreReleaseTag: Bool
let dryRun: Bool let dryRun: Bool
let gitDirectory: String? let gitDirectory: String?
let logLevel: Logger.Level let logLevel: Logger.Level
let configuration: Configuration let target: Configuration.Target?
let branch: Configuration.Branch?
let semvar: Configuration.SemVar?
let configurationFile: String?
public init( public init(
allowPreReleaseTag: Bool = true,
dryRun: Bool = false, dryRun: Bool = false,
gitDirectory: String? = nil, gitDirectory: String? = nil,
logLevel: Logger.Level = .debug, 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.dryRun = dryRun
self.gitDirectory = gitDirectory self.gitDirectory = gitDirectory
self.logLevel = logLevel self.logLevel = logLevel
self.configuration = configuration self.target = target
self.branch = branch
self.semvar = semvar
self.configurationFile = configurationFile
} }
var allowPreReleaseTag: Bool { public init(
guard let semvar = configuration.strategy?.semvar else { return false } allowPreReleaseTag: Bool = true,
return semvar.preRelease != nil 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
)
} }
} }

View File

@@ -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<T>(
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)
}

View File

@@ -1,3 +1,4 @@
import ConfigurationClient
import Dependencies import Dependencies
import FileClient import FileClient
import Foundation import Foundation
@@ -13,11 +14,27 @@ public extension CliClient.SharedOptions {
try await withDependencies { try await withDependencies {
$0.logger.logLevel = logLevel $0.logger.logLevel = logLevel
} operation: { } operation: {
// Load the default configuration, if it exists.
try await withConfiguration(path: configurationFile) { configuration in
@Dependency(\.logger) var logger @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.
let targetUrl = try configuration.targetUrl(gitDirectory: gitDirectory) let targetUrl = try configuration.targetUrl(gitDirectory: gitDirectory)
logger.debug("Target: \(targetUrl.cleanFilePath)") logger.debug("Target: \(targetUrl.cleanFilePath)")
// Perform the operation, which generates the new version and writes it.
try await operation( try await operation(
configuration.currentVersion( configuration.currentVersion(
targetUrl: targetUrl, targetUrl: targetUrl,
@@ -25,9 +42,11 @@ public extension CliClient.SharedOptions {
) )
) )
// Return the file path we wrote the version to.
return targetUrl.cleanFilePath return targetUrl.cleanFilePath
} }
} }
}
func write(_ string: String, to url: URL) async throws { func write(_ string: String, to url: URL) async throws {
@Dependency(\.fileClient) var fileClient @Dependency(\.fileClient) var fileClient

View File

@@ -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)
// )
// }
// }
// }

View File

@@ -7,38 +7,10 @@ extension CliVersionCommand {
struct Build: AsyncParsableCommand { struct Build: AsyncParsableCommand {
static let configuration: CommandConfiguration = .init( static let configuration: CommandConfiguration = .init(
abstract: "Used for the build with version plugin.", abstract: "Used for the build with version plugin.",
discussion: "This should generally not be interacted with directly, outside of the build 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."
) )
@OptionGroup var globals: GlobalBranchOptions @OptionGroup var globals: GlobalOptions
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
func run() async throws { func run() async throws {
try await globals.shared().run(\.build) try await globals.shared().run(\.build)

View File

@@ -7,49 +7,15 @@ extension CliVersionCommand {
static let configuration = CommandConfiguration( static let configuration = CommandConfiguration(
commandName: "bump", commandName: "bump",
abstract: "Bump version of a command-line tool.", 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."
) )
@OptionGroup var globals: GlobalBranchOptions @OptionGroup var globals: GlobalOptions
func run() async throws { func run() async throws {
try await globals.shared().run(\.bump, args: nil) 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 {} extension CliClient.BumpOption: EnumerableFlag {}

View File

@@ -1,61 +1,61 @@
import ConfigurationClient // import ConfigurationClient
import Dependencies // import Dependencies
import FileClient // import FileClient
import Foundation // import Foundation
//
extension Configuration { // extension Configuration {
//
func mergingTarget(_ otherTarget: Configuration.Target?) -> Self { // func mergingTarget(_ otherTarget: Configuration.Target?) -> Self {
.init( // .init(
target: otherTarget ?? target, // target: otherTarget ?? target,
strategy: strategy // strategy: strategy
) // )
} // }
//
func mergingStrategy(_ otherStrategy: Configuration.VersionStrategy?) -> Self { // func mergingStrategy(_ otherStrategy: Configuration.VersionStrategy?) -> Self {
.init( // .init(
target: target, // target: target,
strategy: strategy?.merging(otherStrategy) // strategy: strategy?.merging(otherStrategy)
) // )
} // }
} // }
//
extension Configuration.Branch { // extension Configuration.Branch {
func merging(_ other: Self?) -> Self { // func merging(_ other: Self?) -> Self {
return .init(includeCommitSha: other?.includeCommitSha ?? includeCommitSha) // return .init(includeCommitSha: other?.includeCommitSha ?? includeCommitSha)
} // }
} // }
//
extension Configuration.SemVar { // extension Configuration.SemVar {
func merging(_ other: Self?) -> Self { // func merging(_ other: Self?) -> Self {
.init( // .init(
preRelease: other?.preRelease ?? preRelease, // preRelease: other?.preRelease ?? preRelease,
requireExistingFile: other?.requireExistingFile ?? requireExistingFile, // requireExistingFile: other?.requireExistingFile ?? requireExistingFile,
requireExistingSemVar: other?.requireExistingSemVar ?? requireExistingSemVar // requireExistingSemVar: other?.requireExistingSemVar ?? requireExistingSemVar
) // )
} // }
} // }
//
extension Configuration.VersionStrategy { // extension Configuration.VersionStrategy {
func merging(_ other: Self?) -> Self { // func merging(_ other: Self?) -> Self {
guard let branch else { // guard let branch else {
guard let semvar else { return self } // guard let semvar else { return self }
return .semvar(semvar.merging(other?.semvar)) // return .semvar(semvar.merging(other?.semvar))
} // }
return .branch(branch.merging(other?.branch)) // return .branch(branch.merging(other?.branch))
} // }
} // }
//
@discardableResult // @discardableResult
func withConfiguration<T>( // func withConfiguration<T>(
path: String?, // path: String?,
_ operation: (Configuration) async throws -> T // _ operation: (Configuration) async throws -> T
) async throws -> T { // ) async throws -> T {
@Dependency(\.configurationClient) var configurationClient // @Dependency(\.configurationClient) var configurationClient
//
let configuration = try await configurationClient.findAndLoad( // let configuration = try await configurationClient.findAndLoad(
path != nil ? URL(filePath: path!) : nil // path != nil ? URL(filePath: path!) : nil
) // )
//
return try await operation(configuration) // return try await operation(configuration)
} // }

View File

@@ -8,45 +8,13 @@ extension CliVersionCommand {
struct Generate: AsyncParsableCommand { struct Generate: AsyncParsableCommand {
static let configuration: CommandConfiguration = .init( 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.", 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." discussion: "This command can be interacted with directly, outside of the plugin usage context."
) )
@OptionGroup var globals: GlobalBranchOptions @OptionGroup var globals: GlobalOptions
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
func run() async throws { func run() async throws {
try await globals.shared().run(\.generate) try await globals.shared().run(\.generate)
} }
} }
} }
private enum GenerationError: Error {
case fileExists(path: String)
}

View File

@@ -5,7 +5,7 @@ import Dependencies
import Foundation import Foundation
import Rainbow import Rainbow
struct GlobalOptions<Child: ParsableArguments>: ParsableArguments { struct GlobalOptions: ParsableArguments {
@Option( @Option(
name: .shortAndLong, name: .shortAndLong,
@@ -14,7 +14,17 @@ struct GlobalOptions<Child: ParsableArguments>: ParsableArguments {
var configurationFile: String? var configurationFile: String?
@OptionGroup var targetOptions: TargetOptions @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( @Option(
name: .customLong("git-directory"), name: .customLong("git-directory"),
@@ -36,8 +46,6 @@ struct GlobalOptions<Child: ParsableArguments>: ParsableArguments {
} }
struct Empty: ParsableArguments {}
struct TargetOptions: ParsableArguments { struct TargetOptions: ParsableArguments {
@Option( @Option(
name: .shortAndLong, 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 { struct PreReleaseOptions: ParsableArguments {
@Flag(
name: .shortAndLong,
help: ""
)
var disablePreRelease: Bool = false
@Flag( @Flag(
name: [.customShort("s"), .customLong("pre-release-branch-style")], name: [.customShort("s"), .customLong("pre-release-branch-style")],
help: """ help: """
@@ -76,6 +93,14 @@ struct PreReleaseOptions: ParsableArguments {
) )
var useTagAsPreRelease: Bool = false var useTagAsPreRelease: Bool = false
@Option(
name: .long,
help: """
Add / use a pre-release prefix string.
"""
)
var preReleasePrefix: String?
@Option( @Option(
name: .long, name: .long,
help: """ help: """
@@ -84,6 +109,7 @@ struct PreReleaseOptions: ParsableArguments {
""" """
) )
var custom: String? var custom: String?
} }
struct SemVarOptions: ParsableArguments { struct SemVarOptions: ParsableArguments {
@@ -105,25 +131,7 @@ struct SemVarOptions: ParsableArguments {
@OptionGroup var preRelease: PreReleaseOptions @OptionGroup var preRelease: PreReleaseOptions
} }
typealias GlobalSemVarOptions = GlobalOptions<SemVarOptions> // TODO: Move these to global options.
typealias GlobalBranchOptions = GlobalOptions<Empty>
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()))
}
}
}
extension CliClient.SharedOptions { extension CliClient.SharedOptions {
func run(_ keyPath: KeyPath<CliClient, @Sendable (Self) async throws -> String>) async throws { func run(_ keyPath: KeyPath<CliClient, @Sendable (Self) async throws -> String>) async throws {
@@ -156,12 +164,16 @@ extension CliClient.SharedOptions {
extension GlobalOptions { extension GlobalOptions {
func shared(_ configuration: Configuration) throws -> CliClient.SharedOptions { func shared() throws -> CliClient.SharedOptions {
.init( try .init(
allowPreReleaseTag: !semvarOptions.preRelease.disablePreRelease,
dryRun: dryRun, dryRun: dryRun,
gitDirectory: gitDirectory, gitDirectory: gitDirectory,
logLevel: .init(verbose: verbose), verbose: verbose,
configuration: configuration target: targetOptions.configTarget(),
branch: .init(includeCommitSha: commitSha),
semvar: semvarOptions.configSemVarOptions(),
configurationFile: configurationFile
) )
} }
@@ -170,20 +182,6 @@ extension GlobalOptions {
// MARK: - Helpers // MARK: - Helpers
private extension TargetOptions { 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? { func configTarget() throws -> Configuration.Target? {
guard let path else { guard let path else {
guard let module else { guard let module else {

View File

@@ -133,14 +133,17 @@ extension CliClient.SharedOptions {
logLevel: Logger.Level = .trace, logLevel: Logger.Level = .trace,
versionStrategy: Configuration.VersionStrategy = .semvar(.init()) versionStrategy: Configuration.VersionStrategy = .semvar(.init())
) -> Self { ) -> Self {
.init( return .init(
dryRun: dryRun, dryRun: dryRun,
gitDirectory: gitDirectory, gitDirectory: gitDirectory,
logLevel: logLevel, logLevel: logLevel,
configuration: .init(
target: .init(module: .init(target)), target: .init(module: .init(target)),
strategy: versionStrategy branch: versionStrategy.branch,
) semvar: versionStrategy.semvar
// configuration: .init(
// target: .init(module: .init(target)),
// strategy: versionStrategy
// )
) )
} }
} }