feat: Updates logging configuration.

This commit is contained in:
2024-12-24 21:32:14 -05:00
parent 04bfd4a6ae
commit a885e3dfa3
15 changed files with 335 additions and 107 deletions

View File

@@ -40,7 +40,8 @@ let package = Package(
"ConfigurationClient", "ConfigurationClient",
"FileClient", "FileClient",
"GitClient", "GitClient",
.product(name: "Logging", package: "swift-log") .product(name: "Logging", package: "swift-log"),
.product(name: "CustomDump", package: "swift-custom-dump")
] ]
), ),
.testTarget( .testTarget(

View File

@@ -34,12 +34,30 @@ public struct CliClient: Sendable {
case major, minor, patch, preRelease case major, minor, patch, preRelease
} }
// TODO: Need a quiet option, as default log level is warning, need a way to set it to ignore logs.
public struct LoggingOptions: Equatable, Sendable {
let command: String
let executableName: String
let verbose: Int
public init(
executableName: String = "bump-version",
command: String,
verbose: Int
) {
self.executableName = executableName
self.command = command
self.verbose = verbose
}
}
public struct SharedOptions: Equatable, Sendable { public struct SharedOptions: Equatable, Sendable {
let allowPreReleaseTag: Bool let allowPreReleaseTag: Bool
let dryRun: Bool let dryRun: Bool
let gitDirectory: String? let gitDirectory: String?
let logLevel: Logger.Level let loggingOptions: LoggingOptions
let target: Configuration.Target? let target: Configuration.Target?
let branch: Configuration.Branch? let branch: Configuration.Branch?
let semvar: Configuration.SemVar? let semvar: Configuration.SemVar?
@@ -49,7 +67,7 @@ public struct CliClient: Sendable {
allowPreReleaseTag: Bool = true, allowPreReleaseTag: Bool = true,
dryRun: Bool = false, dryRun: Bool = false,
gitDirectory: String? = nil, gitDirectory: String? = nil,
logLevel: Logger.Level = .debug, loggingOptions: LoggingOptions,
target: Configuration.Target? = nil, target: Configuration.Target? = nil,
branch: Configuration.Branch? = nil, branch: Configuration.Branch? = nil,
semvar: Configuration.SemVar? = nil, semvar: Configuration.SemVar? = nil,
@@ -58,34 +76,12 @@ public struct CliClient: Sendable {
self.allowPreReleaseTag = allowPreReleaseTag self.allowPreReleaseTag = allowPreReleaseTag
self.dryRun = dryRun self.dryRun = dryRun
self.gitDirectory = gitDirectory self.gitDirectory = gitDirectory
self.logLevel = logLevel self.loggingOptions = loggingOptions
self.target = target self.target = target
self.branch = branch self.branch = branch
self.semvar = semvar self.semvar = semvar
self.configurationFile = configurationFile self.configurationFile = configurationFile
} }
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
)
}
} }
} }

View File

@@ -4,4 +4,5 @@ enum CliClientError: Error {
case fileDoesNotExist(path: String) case fileDoesNotExist(path: String)
case failedToParseVersionFile case failedToParseVersionFile
case semVarNotFound case semVarNotFound
case preReleaseParsingError(String)
} }

View File

@@ -1,4 +1,5 @@
import ConfigurationClient import ConfigurationClient
import CustomDump
import Dependencies import Dependencies
import FileClient import FileClient
import Foundation import Foundation
@@ -7,6 +8,40 @@ import GitClient
@_spi(Internal) @_spi(Internal)
public extension CliClient.SharedOptions { public extension CliClient.SharedOptions {
/// All cli-client calls should run through this, it set's up logging,
/// loads configuration, and generates the current version based on the
/// configuration.
@discardableResult
func run(
_ operation: (CurrentVersionContainer) async throws -> Void
) async rethrows -> String {
try await loggingOptions.withLogger {
// Load the default configuration, if it exists.
try await withMergedConfiguration { configuration in
@Dependency(\.logger) var logger
var configurationString = ""
customDump(configuration, to: &configurationString)
logger.trace("\nConfiguration: \(configurationString)")
// 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 the file path we wrote the version to.
return targetUrl.cleanFilePath
}
}
}
// Merges any configuration set via the passed in options. // Merges any configuration set via the passed in options.
@discardableResult @discardableResult
func withMergedConfiguration<T>( func withMergedConfiguration<T>(
@@ -26,37 +61,6 @@ public extension CliClient.SharedOptions {
} }
} }
@discardableResult
func run(
_ operation: (CurrentVersionContainer) async throws -> Void
) async rethrows -> String {
try await withDependencies {
$0.logger.logLevel = logLevel
} operation: {
// Load the default configuration, if it exists.
try await withMergedConfiguration { configuration in
@Dependency(\.logger) var logger
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 the file path we wrote the version to.
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
@Dependency(\.logger) var logger @Dependency(\.logger) var logger
@@ -64,7 +68,7 @@ public extension CliClient.SharedOptions {
try await fileClient.write(string: string, to: url) try await fileClient.write(string: string, to: url)
} else { } else {
logger.debug("Skipping, due to dry-run being passed.") logger.debug("Skipping, due to dry-run being passed.")
logger.debug("\(string)") logger.debug("\n\(string)\n")
} }
} }

View File

@@ -20,6 +20,15 @@ extension Configuration {
} }
} }
extension Configuration.PreRelease {
func merging(_ other: Self?) -> Self {
.init(
prefix: other?.prefix ?? prefix,
strategy: other?.strategy ?? strategy
)
}
}
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)
@@ -29,7 +38,7 @@ extension Configuration.Branch {
extension Configuration.SemVar { extension Configuration.SemVar {
func merging(_ other: Self?) -> Self { func merging(_ other: Self?) -> Self {
.init( .init(
preRelease: other?.preRelease ?? preRelease, preRelease: preRelease?.merging(other?.preRelease),
requireExistingFile: other?.requireExistingFile ?? requireExistingFile, requireExistingFile: other?.requireExistingFile ?? requireExistingFile,
requireExistingSemVar: other?.requireExistingSemVar ?? requireExistingSemVar requireExistingSemVar: other?.requireExistingSemVar ?? requireExistingSemVar
) )

View File

@@ -2,6 +2,7 @@ import ConfigurationClient
import Dependencies import Dependencies
import Foundation import Foundation
import GitClient import GitClient
import ShellClient
extension Configuration { extension Configuration {
func targetUrl(gitDirectory: String?) throws -> URL { func targetUrl(gitDirectory: String?) throws -> URL {
@@ -58,26 +59,46 @@ extension Configuration.Target {
} }
extension GitClient { extension GitClient {
func version(branch: Configuration.Branch, gitDirectory: String?) async throws -> String { func version(includeCommitSha: Bool, gitDirectory: String?) async throws -> String {
@Dependency(\.gitClient) var gitClient @Dependency(\.gitClient) var gitClient
return try await gitClient.version(.init( return try await gitClient.version(.init(
gitDirectory: gitDirectory, gitDirectory: gitDirectory,
style: .branch(commitSha: branch.includeCommitSha) style: .branch(commitSha: includeCommitSha)
)).description )).description
} }
} }
extension Configuration.PreRelease { extension Configuration.PreRelease {
func preReleaseString(gitDirectory: String?) async throws -> String { // FIX: This needs to handle the pre-release type appropriatly.
func preReleaseString(gitDirectory: String?) async throws -> PreReleaseString? {
guard let strategy else { return nil }
@Dependency(\.asyncShellClient) var asyncShellClient
@Dependency(\.gitClient) var gitClient @Dependency(\.gitClient) var gitClient
@Dependency(\.logger) var logger
let preReleaseString: String var preReleaseString: String
var suffix = true
var allowsPrefix = true
if let branch = strategy?.branch { switch strategy {
preReleaseString = try await gitClient.version(branch: branch, gitDirectory: gitDirectory) case let .branch(includeCommitSha: includeCommitSha):
} else { logger.trace("Branch pre-release strategy, includeCommitSha: \(includeCommitSha).")
preReleaseString = try await gitClient.version(
includeCommitSha: includeCommitSha,
gitDirectory: gitDirectory
)
case let .command(arguments: arguments):
logger.trace("Command pre-release strategy, arguments: \(arguments).")
// TODO: What to do with allows prefix? Need a configuration setting for commands.
preReleaseString = try await asyncShellClient.background(.init(arguments))
case .gitTag:
logger.trace("Git tag pre-release strategy.")
logger.trace("This will ignore any set prefix.")
suffix = false
allowsPrefix = false
preReleaseString = try await gitClient.version(.init( preReleaseString = try await gitClient.version(.init(
gitDirectory: gitDirectory, gitDirectory: gitDirectory,
style: .tag(exactMatch: false) style: .tag(exactMatch: false)
@@ -85,9 +106,22 @@ extension Configuration.PreRelease {
} }
if let prefix { if let prefix {
return "\(prefix)-\(preReleaseString)" if allowsPrefix {
preReleaseString = "\(prefix)-\(preReleaseString)"
} else {
logger.warning("Found prefix, but pre-release strategy may not work properly, ignoring prefix.")
}
} }
return preReleaseString
guard suffix else { return .semvar(preReleaseString) }
return .suffix(preReleaseString)
// return preReleaseString
}
enum PreReleaseString: Sendable {
case suffix(String)
case semvar(String)
} }
} }
@@ -97,15 +131,28 @@ public extension Configuration.SemVar {
private func applyingPreRelease(_ semVar: SemVar, _ gitDirectory: String?) async throws -> SemVar { private func applyingPreRelease(_ semVar: SemVar, _ gitDirectory: String?) async throws -> SemVar {
@Dependency(\.logger) var logger @Dependency(\.logger) var logger
logger.trace("Start apply pre-release to: \(semVar)") logger.trace("Start apply pre-release to: \(semVar)")
guard let preReleaseStrategy = self.preRelease else {
guard let preReleaseStrategy = self.preRelease,
let preRelease = try await preReleaseStrategy.preReleaseString(gitDirectory: gitDirectory)
else {
logger.trace("No pre-release strategy, returning original semvar.") logger.trace("No pre-release strategy, returning original semvar.")
return semVar return semVar
} }
let preRelease = try await preReleaseStrategy.preReleaseString(gitDirectory: gitDirectory) // let preRelease = try await preReleaseStrategy.preReleaseString(gitDirectory: gitDirectory)
logger.trace("Pre-release string: \(preRelease)") logger.trace("Pre-release string: \(preRelease)")
return semVar.applyingPreRelease(preRelease) switch preRelease {
case let .suffix(string):
return semVar.applyingPreRelease(string)
case let .semvar(string):
guard let semvar = SemVar(string: string) else {
throw CliClientError.preReleaseParsingError(string)
}
return semvar
}
// return semVar.applyingPreRelease(preRelease)
} }
func currentVersion(file: URL, gitDirectory: String? = nil) async throws -> CurrentVersionContainer.Version { func currentVersion(file: URL, gitDirectory: String? = nil) async throws -> CurrentVersionContainer.Version {
@@ -181,7 +228,7 @@ extension Configuration.VersionStrategy {
return try await .init( return try await .init(
targetUrl: targetUrl, targetUrl: targetUrl,
version: .string( version: .string(
gitClient.version(branch: branch, gitDirectory: gitDirectory) gitClient.version(includeCommitSha: branch.includeCommitSha, gitDirectory: gitDirectory)
) )
) )
} }

View File

@@ -28,13 +28,13 @@ public extension FileClient {
logger.debug("Version line: \(versionLine)") logger.debug("Version line: \(versionLine)")
let isOptional = versionLine.contains("String?") let isOptional = versionLine.contains("String?")
logger.debug("Uses optional: \(isOptional)") logger.trace("Uses optional: \(isOptional)")
let versionString = versionLine.split(separator: "let VERSION: \(isOptional ? "String?" : "String") = ").last let versionString = versionLine.split(separator: "let VERSION: \(isOptional ? "String?" : "String") = ").last
guard let versionString else { guard let versionString else {
throw CliClientError.failedToParseVersionFile throw CliClientError.failedToParseVersionFile
} }
logger.debug("Parsed version string: \(versionString)") logger.trace("Parsed version string: \(versionString)")
return (String(versionString), isOptional) return (String(versionString), isOptional)
} }

View File

@@ -1,15 +0,0 @@
import Logging
// TODO: Move.
@_spi(Internal)
public extension Logger.Level {
init(verbose: Int) {
switch verbose {
case 1: self = .warning
case 2: self = .debug
case 3...: self = .trace
default: self = .info
}
}
}

View File

@@ -0,0 +1,172 @@
import Dependencies
import Foundation
import Logging
import LoggingFormatAndPipe
import Rainbow
import ShellClient
extension String {
var orange: Self {
bit24(255, 165, 0)
}
var magena: Self {
// bit24(186, 85, 211)
bit24(238, 130, 238)
}
}
@_spi(Internal)
public extension Logger.Level {
init(verbose: Int) {
switch verbose {
case 1: self = .debug
case 2...: self = .trace
default: self = .warning
}
}
var coloredString: String {
switch self {
case .info:
return "\(self)".cyan
case .warning:
return "\(self)".orange.bold
case .debug:
return "\(self)".green
case .trace:
return "\(self)".yellow
case .error:
return "\(self)".red.bold
default:
return "\(self)"
}
}
}
struct LevelFormatter: LoggingFormatAndPipe.Formatter {
let basic: BasicFormatter
var timestampFormatter: DateFormatter { basic.timestampFormatter }
// swiftlint:disable function_parameter_count
func processLog(
level: Logger.Level,
message: Logger.Message,
prettyMetadata: String?,
file: String,
function: String,
line: UInt
) -> String {
let now = Date()
return basic.format.map { component -> String in
return processComponent(
component,
now: now,
level: level,
message: message,
prettyMetadata: prettyMetadata,
file: file,
function: function,
line: line
)
}
.filter { $0.count > 0 }
.joined(separator: basic.separator ?? "")
}
public func processComponent(
_ component: LogComponent,
now: Date,
level: Logger.Level,
message: Logger.Message,
prettyMetadata: String?,
file: String,
function: String,
line: UInt
) -> String {
switch component {
case .level:
let maxLen = "\(Logger.Level.warning)".count
let paddingCount = (maxLen - "\(level)".count) / 2
var padding = ""
for _ in 0 ... paddingCount {
padding += " "
}
return "\(padding)\(level.coloredString)\(padding)"
case let .group(components):
return components.map { component -> String in
self.processComponent(
component,
now: now,
level: level,
message: message,
prettyMetadata: prettyMetadata,
file: file,
function: function,
line: line
)
}.joined()
case .message:
return basic.processComponent(
component,
now: now,
level: level,
message: message,
prettyMetadata: prettyMetadata,
file: file,
function: function,
line: line
).italic
default:
return basic.processComponent(
component,
now: now,
level: level,
message: message,
prettyMetadata: prettyMetadata,
file: file,
function: function,
line: line
)
}
}
// swiftlint:enable function_parameter_count
}
extension CliClient.LoggingOptions {
func makeLogger() -> Logger {
let formatters: [LogComponent] = [
.text(executableName.magena),
.text(command.blue),
.level,
.group([
.text("-> "),
.message
])
]
return Logger(label: executableName) { _ in
LoggingFormatAndPipe.Handler(
formatter: LevelFormatter(basic: BasicFormatter(
formatters,
separator: " | "
)),
pipe: LoggerTextOutputStreamPipe.standardOutput
)
}
}
func withLogger<T>(_ operation: () async throws -> T) async rethrows -> T {
try await withDependencies {
$0.logger = makeLogger()
$0.logger.logLevel = .init(verbose: verbose)
} operation: {
try await operation()
}
}
}

View File

@@ -4,8 +4,10 @@ import Foundation
import ShellClient import ShellClient
struct BuildCommand: AsyncParsableCommand { struct BuildCommand: AsyncParsableCommand {
static let commandName = "build"
static let configuration: CommandConfiguration = .init( static let configuration: CommandConfiguration = .init(
commandName: "build", commandName: Self.commandName,
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.",
shouldDisplay: false shouldDisplay: false
@@ -14,6 +16,6 @@ struct BuildCommand: AsyncParsableCommand {
@OptionGroup var globals: GlobalOptions @OptionGroup var globals: GlobalOptions
func run() async throws { func run() async throws {
try await globals.run(\.build) try await globals.run(\.build, command: Self.commandName)
} }
} }

View File

@@ -4,8 +4,10 @@ import Dependencies
struct BumpCommand: AsyncParsableCommand { struct BumpCommand: AsyncParsableCommand {
static let commandName = "bump"
static let configuration = CommandConfiguration( static let configuration = CommandConfiguration(
commandName: "bump", commandName: Self.commandName,
abstract: "Bump version of a command-line tool." abstract: "Bump version of a command-line tool."
) )
@@ -19,7 +21,7 @@ struct BumpCommand: AsyncParsableCommand {
var bumpOption: CliClient.BumpOption = .patch var bumpOption: CliClient.BumpOption = .patch
func run() async throws { func run() async throws {
try await globals.run(\.bump, args: bumpOption) try await globals.run(\.bump, command: Self.commandName, args: bumpOption)
} }
} }

View File

@@ -5,8 +5,10 @@ import Foundation
import ShellClient import ShellClient
struct GenerateCommand: AsyncParsableCommand { struct GenerateCommand: AsyncParsableCommand {
static let commandName = "generate"
static let configuration: CommandConfiguration = .init( static let configuration: CommandConfiguration = .init(
commandName: "generate", commandName: Self.commandName,
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." discussion: "This command can be interacted with directly, outside of the plugin usage context."
) )
@@ -14,6 +16,6 @@ struct GenerateCommand: AsyncParsableCommand {
@OptionGroup var globals: GlobalOptions @OptionGroup var globals: GlobalOptions
func run() async throws { func run() async throws {
try await globals.run(\.generate) try await globals.run(\.generate, command: Self.commandName)
} }
} }

View File

@@ -18,8 +18,10 @@ struct UtilsCommand: AsyncParsableCommand {
extension UtilsCommand { extension UtilsCommand {
struct DumpConfig: AsyncParsableCommand { struct DumpConfig: AsyncParsableCommand {
static let commandName = "dump-config"
static let configuration = CommandConfiguration( static let configuration = CommandConfiguration(
commandName: "dump-config", commandName: Self.commandName,
abstract: "Show the parsed configuration.", abstract: "Show the parsed configuration.",
aliases: ["dc"] aliases: ["dc"]
) )
@@ -27,7 +29,7 @@ extension UtilsCommand {
@OptionGroup var globals: GlobalOptions @OptionGroup var globals: GlobalOptions
func run() async throws { func run() async throws {
let configuration = try await globals.runClient(\.parsedConfiguration) let configuration = try await globals.runClient(\.parsedConfiguration, command: Self.commandName)
customDump(configuration) customDump(configuration)
} }
} }

View File

@@ -21,45 +21,49 @@ func withSetupDependencies<T>(
extension GlobalOptions { extension GlobalOptions {
func runClient<T>( func runClient<T>(
_ keyPath: KeyPath<CliClient, @Sendable (CliClient.SharedOptions) async throws -> T> _ keyPath: KeyPath<CliClient, @Sendable (CliClient.SharedOptions) async throws -> T>,
command: String
) async throws -> T { ) async throws -> T {
try await withSetupDependencies { try await withSetupDependencies {
@Dependency(\.cliClient) var cliClient @Dependency(\.cliClient) var cliClient
return try await cliClient[keyPath: keyPath](shared()) return try await cliClient[keyPath: keyPath](shared(command: command))
} }
} }
func runClient<A, T>( func runClient<A, T>(
_ keyPath: KeyPath<CliClient, @Sendable (A, CliClient.SharedOptions) async throws -> T>, _ keyPath: KeyPath<CliClient, @Sendable (A, CliClient.SharedOptions) async throws -> T>,
command: String,
args: A args: A
) async throws -> T { ) async throws -> T {
try await withSetupDependencies { try await withSetupDependencies {
@Dependency(\.cliClient) var cliClient @Dependency(\.cliClient) var cliClient
return try await cliClient[keyPath: keyPath](args, shared()) return try await cliClient[keyPath: keyPath](args, shared(command: command))
} }
} }
func run( func run(
_ keyPath: KeyPath<CliClient, @Sendable (CliClient.SharedOptions) async throws -> String> _ keyPath: KeyPath<CliClient, @Sendable (CliClient.SharedOptions) async throws -> String>,
command: String
) async throws { ) async throws {
let output = try await runClient(keyPath) let output = try await runClient(keyPath, command: command)
print(output) print(output)
} }
func run<T>( func run<T>(
_ keyPath: KeyPath<CliClient, @Sendable (T, CliClient.SharedOptions) async throws -> String>, _ keyPath: KeyPath<CliClient, @Sendable (T, CliClient.SharedOptions) async throws -> String>,
command: String,
args: T args: T
) async throws { ) async throws {
let output = try await runClient(keyPath, args: args) let output = try await runClient(keyPath, command: command, args: args)
print(output) print(output)
} }
func shared() throws -> CliClient.SharedOptions { func shared(command: String) throws -> CliClient.SharedOptions {
try .init( try .init(
allowPreReleaseTag: !configOptions.semvarOptions.preRelease.disablePreRelease, allowPreReleaseTag: !configOptions.semvarOptions.preRelease.disablePreRelease,
dryRun: dryRun, dryRun: dryRun,
gitDirectory: gitDirectory, gitDirectory: gitDirectory,
verbose: verbose, loggingOptions: .init(command: command, verbose: verbose),
target: configOptions.target(), target: configOptions.target(),
branch: .init(includeCommitSha: configOptions.commitSha), branch: .init(includeCommitSha: configOptions.commitSha),
semvar: configOptions.semvarOptions(extraOptions: extraOptions), semvar: configOptions.semvarOptions(extraOptions: extraOptions),
@@ -93,6 +97,8 @@ extension PreReleaseOptions {
throw ExtraOptionsEmpty() throw ExtraOptionsEmpty()
} }
return .init(prefix: preReleasePrefix, strategy: .command(arguments: extraOptions)) return .init(prefix: preReleasePrefix, strategy: .command(arguments: extraOptions))
} else if let preReleasePrefix {
return .init(prefix: preReleasePrefix, strategy: nil)
} }
return nil return nil
} }

View File

@@ -132,13 +132,12 @@ extension CliClient.SharedOptions {
gitDirectory: String? = "/baz", gitDirectory: String? = "/baz",
dryRun: Bool = false, dryRun: Bool = false,
target: String = "bar", target: String = "bar",
logLevel: Logger.Level = .trace,
versionStrategy: Configuration.VersionStrategy = .semvar(.init()) versionStrategy: Configuration.VersionStrategy = .semvar(.init())
) -> Self { ) -> Self {
return .init( return .init(
dryRun: dryRun, dryRun: dryRun,
gitDirectory: gitDirectory, gitDirectory: gitDirectory,
logLevel: logLevel, loggingOptions: .init(command: "test", verbose: 2),
target: .init(module: .init(target)), target: .init(module: .init(target)),
branch: versionStrategy.branch, branch: versionStrategy.branch,
semvar: versionStrategy.semvar semvar: versionStrategy.semvar