Compare commits

...

3 Commits

12 changed files with 454 additions and 398 deletions

View File

@@ -87,6 +87,7 @@ let package = Package(
.target(
name: "LoggingExtensions",
dependencies: [
.product(name: "CustomDump", package: "swift-custom-dump"),
.product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "ShellClient", package: "swift-shell-client")
]

View File

@@ -1,8 +1,12 @@
import ConfigurationClient
enum CliClientError: Error {
case gitDirectoryNotFound
case fileExists(path: String)
case fileDoesNotExist(path: String)
case failedToParseVersionFile
case semVarNotFound
case semVarNotFound(message: String)
case strategyNotFound(configuration: Configuration)
case preReleaseParsingError(String)
case versionStringNotFound
}

View File

@@ -4,25 +4,29 @@ import Dependencies
import FileClient
import Foundation
import GitClient
import LoggingExtensions
@_spi(Internal)
public extension CliClient.SharedOptions {
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
_ operation: (VersionContainer) 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)")
guard let strategy = configuration.strategy else {
throw CliClientError.strategyNotFound(configuration: configuration)
}
logger.dump(configuration, level: .trace) {
"\nConfiguration: \($0)"
}
// This will fail if the target url is not set properly.
let targetUrl = try configuration.targetUrl(gitDirectory: projectDirectory)
@@ -30,10 +34,7 @@ public extension CliClient.SharedOptions {
// Perform the operation, which generates the new version and writes it.
try await operation(
configuration.currentVersion(
targetUrl: targetUrl,
gitDirectory: projectDirectory
)
.load(projectDirectory: projectDirectory, strategy: strategy, url: targetUrl)
)
// Return the file path we wrote the version to.
@@ -54,10 +55,9 @@ public extension CliClient.SharedOptions {
logger.trace("Merging branch strategy.")
// strategy = .branch(branch)
} else if let semvar = configurationToMerge?.strategy?.semvar {
logger.trace("Merging semvar strategy.")
var semvarString = ""
customDump(semvar, to: &semvarString)
logger.trace("\(semvarString)")
logger.dump(semvar, level: .trace) {
"Merging semvar strategy:\n\($0)"
}
}
return try await configurationClient.withConfiguration(
@@ -75,63 +75,52 @@ public extension CliClient.SharedOptions {
try await fileClient.write(string: string, to: url)
} else {
logger.debug("Skipping, due to dry-run being passed.")
// logger.info("\n\(string)\n")
}
}
func write(_ currentVersion: CurrentVersionContainer) async throws {
func write(_ currentVersion: VersionContainer) async throws {
@Dependency(\.logger) var logger
logger.trace("Begin writing version.")
let version = try currentVersion.version.string(allowPreReleaseTag: allowPreReleaseTag)
if !dryRun {
logger.debug("Version: \(version)")
} else {
logger.info("Version: \(version)")
let hasChanges: Bool
let targetUrl: URL
let usesOptionalType: Bool
let versionString: String?
switch currentVersion {
case let .branch(branch):
hasChanges = branch.hasChanges
targetUrl = branch.targetUrl
usesOptionalType = branch.usesOptionalType
versionString = branch.versionString
case let .semvar(semvar):
hasChanges = semvar.hasChanges
targetUrl = semvar.targetUrl
usesOptionalType = semvar.usesOptionalType
versionString = semvar.versionString(withPreRelease: allowPreReleaseTag)
}
let template = currentVersion.usesOptionalType ? Template.optional(version) : Template.nonOptional(version)
// if !hasChanges {
// logger.debug("No changes from loaded version, not writing next version.")
// return
// }
guard let versionString else {
throw CliClientError.versionStringNotFound
}
// let version = try currentVersion.version.string(allowPreReleaseTag: allowPreReleaseTag)
if !dryRun {
logger.debug("Version: \(versionString)")
} else {
logger.info("Version: \(versionString)")
}
let template = usesOptionalType ? Template.optional(versionString) : Template.nonOptional(versionString)
logger.trace("Template string: \(template)")
try await write(template, to: currentVersion.targetUrl)
}
}
// TODO: Add optional property for currentVersion (loaded version from file)
// and rename version to nextVersion.
@_spi(Internal)
public struct CurrentVersionContainer: Sendable {
let targetUrl: URL
let currentVersion: CurrentVersion?
let version: Version
var usesOptionalType: Bool {
switch version {
case .string: return false
case let .semvar(_, usesOptionalType, _): return usesOptionalType
}
}
public enum CurrentVersion: Sendable {
case branch(String, usesOptionalType: Bool)
case semvar(SemVar, usesOptionalType: Bool)
}
public enum Version: Sendable {
// TODO: Call this branch for consistency.
case string(String)
// TODO: Remove has changes when currentVersion/nextVersion is implemented.
case semvar(SemVar, usesOptionalType: Bool = true, hasChanges: Bool)
func string(allowPreReleaseTag: Bool) throws -> String {
switch self {
case let .string(string):
return string
case let .semvar(semvar, usesOptionalType: _, hasChanges: _):
return semvar.versionString(withPreReleaseTag: allowPreReleaseTag)
}
}
try await write(template, to: targetUrl)
}
}
@@ -152,30 +141,30 @@ extension CliClient.SharedOptions {
@Dependency(\.logger) var logger
switch container.version {
case .string: // When we did not parse a semvar, just write whatever we parsed for the current version.
switch container {
case .branch: // When we did not parse a semvar, just write whatever we parsed for the current version.
logger.debug("Failed to parse semvar, but got current version string.")
try await write(container)
case let .semvar(semvar, usesOptionalType: usesOptionalType, hasChanges: hasChanges):
logger.debug("Semvar prior to bumping: \(semvar)")
let bumped = semvar.bump(type)
let version = bumped.versionString(withPreReleaseTag: allowPreReleaseTag)
case let .semvar(semvar):
guard bumped != semvar || hasChanges else {
logger.debug("No change, skipping.")
return
let version: SemVar?
switch semvar.precedence ?? .default {
case .file:
version = semvar.loadedVersion ?? semvar.strategyVersion
case .strategy:
version = semvar.strategyVersion ?? semvar.loadedVersion
}
logger.debug("Bumped version: \(version)")
if dryRun {
logger.info("Version: \(version)")
return
// let version = semvar.loadedVersion ?? semvar.nextVersion
guard let version else {
throw CliClientError.semVarNotFound(message: "Failed to parse a valid semvar to bump.")
}
let template = usesOptionalType ? Template.optional(version) : Template.build(version)
try await write(template, to: container.targetUrl)
logger.dump(version, level: .debug) { "Version prior to bumping:\n\($0)" }
let bumped = version.bump(type)
logger.dump(bumped, level: .trace) { "Bumped version:\n\($0)" }
try await write(.semvar(semvar.withUpdateNextVersion(bumped)))
}
}
}

View File

@@ -0,0 +1,57 @@
import ConfigurationClient
import CustomDump
import Dependencies
import Foundation
import GitClient
import ShellClient
extension Configuration {
func targetUrl(gitDirectory: String?) throws -> URL {
guard let target else {
throw ConfigurationParsingError.targetNotFound
}
return try target.url(gitDirectory: gitDirectory)
}
}
private extension Configuration.Target {
func url(gitDirectory: String?) throws -> URL {
@Dependency(\.logger) var logger
let filePath: String
if let path {
filePath = path
} else {
guard let module else {
throw ConfigurationParsingError.pathOrModuleNotSet
}
var path = module.name
logger.debug("module.name: \(path)")
if path.hasPrefix("./") {
path = String(path.dropFirst(2))
}
if !path.hasPrefix("Sources") {
logger.debug("no prefix")
path = "Sources/\(path)"
}
filePath = "\(path)/\(module.fileNameOrDefault)"
}
if let gitDirectory {
return URL(filePath: "\(gitDirectory)/\(filePath)")
}
return URL(filePath: filePath)
}
}
enum ConfigurationParsingError: Error {
case targetNotFound
case pathOrModuleNotSet
case versionStrategyError(message: String)
case versionStrategyNotFound
}

View File

@@ -1,286 +0,0 @@
import ConfigurationClient
import Dependencies
import Foundation
import GitClient
import ShellClient
extension Configuration {
func targetUrl(gitDirectory: String?) throws -> URL {
guard let target else {
throw ConfigurationParsingError.targetNotFound
}
return try target.url(gitDirectory: gitDirectory)
}
func currentVersion(targetUrl: URL, gitDirectory: String?) async throws -> CurrentVersionContainer {
guard let strategy else {
throw ConfigurationParsingError.versionStrategyNotFound
}
return try await strategy.currentVersion(
targetUrl: targetUrl,
gitDirectory: gitDirectory
)
}
}
private extension Configuration.SemVar.Strategy {
func getSemvar(gitDirectory: String? = nil) async throws -> SemVar {
@Dependency(\.asyncShellClient) var asyncShellClient
@Dependency(\.gitClient) var gitClient
@Dependency(\.logger) var logger
let semvar: SemVar?
switch self {
case let .command(arguments: arguments):
logger.trace("Using custom command strategy with: \(arguments)")
semvar = try await SemVar(string: asyncShellClient.background(.init(arguments)))
case let .gitTag(exactMatch: exactMatch):
logger.trace("Using gitTag strategy.")
semvar = try await gitClient.version(.init(
gitDirectory: gitDirectory,
style: .tag(exactMatch: exactMatch ?? false)
)).semVar
}
guard let semvar else {
throw CliClientError.semVarNotFound
}
return semvar
}
}
@_spi(Internal)
public extension Configuration.SemVar {
// TODO: Need to handle custom command semvar strategy.
func currentVersion(file: URL, gitDirectory: String? = nil) async throws -> CurrentVersionContainer.Version {
@Dependency(\.fileClient) var fileClient
@Dependency(\.gitClient) var gitClient
@Dependency(\.logger) var logger
let fileOutput = try? await fileClient.semvar(file: file, gitDirectory: gitDirectory)
var semVar = fileOutput?.semVar
logger.trace("file output semvar: \(String(describing: semVar))")
let usesOptionalType = fileOutput?.usesOptionalType
// We parsed a semvar from the existing file, use it.
if semVar != nil {
let semvarWithPreRelease = try await applyingPreRelease(semVar!, gitDirectory)
return .semvar(
semvarWithPreRelease,
usesOptionalType: usesOptionalType ?? false,
hasChanges: semvarWithPreRelease != semVar
)
}
if requireExistingFile == true {
logger.debug("Failed to parse existing file, and caller requires it.")
throw CliClientError.fileDoesNotExist(path: file.cleanFilePath)
}
logger.trace("Does not require existing file, checking git-tag.")
// TODO: Is this what we want to do here? Seems that the strategy should be set by the client / configuration.
// 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 semVar != nil {
let semvarWithPreRelease = try await applyingPreRelease(semVar!, gitDirectory)
return .semvar(
semvarWithPreRelease,
usesOptionalType: usesOptionalType ?? false,
hasChanges: semvarWithPreRelease != semVar
)
}
if requireExistingSemVar == true {
logger.trace("Caller requires existing semvar and it was not found in file or git-tag.")
throw CliClientError.semVarNotFound
}
// Semvar doesn't exist, so create a new one.
logger.trace("Generating new semvar.")
return try await .semvar(
applyingPreRelease(.init(), gitDirectory),
usesOptionalType: usesOptionalType ?? false,
hasChanges: true
)
}
}
private extension Configuration.VersionStrategy {
// TODO: This should just load the `nextVersion`, and should probably live on CurrentVersionContainer.
// FIX: Fix what's passed to current verions here.
func currentVersion(targetUrl: URL, gitDirectory: String?) async throws -> CurrentVersionContainer {
@Dependency(\.gitClient) var gitClient
guard let branch else {
guard let semvar else {
throw ConfigurationParsingError.versionStrategyError(
message: "Neither branch nor semvar set on configuration."
)
}
return try await .init(
targetUrl: targetUrl,
currentVersion: nil,
version: semvar.currentVersion(file: targetUrl, gitDirectory: gitDirectory)
)
}
return try await .init(
targetUrl: targetUrl,
currentVersion: nil,
version: .string(
gitClient.version(includeCommitSha: branch.includeCommitSha, gitDirectory: gitDirectory)
)
)
}
}
private extension Configuration.Target {
func url(gitDirectory: String?) throws -> URL {
@Dependency(\.logger) var logger
let filePath: String
if let path {
filePath = path
} else {
guard let module else {
throw ConfigurationParsingError.pathOrModuleNotSet
}
var path = module.name
logger.debug("module.name: \(path)")
if path.hasPrefix("./") {
path = String(path.dropFirst(2))
}
if !path.hasPrefix("Sources") {
logger.debug("no prefix")
path = "Sources/\(path)"
}
filePath = "\(path)/\(module.fileNameOrDefault)"
}
if let gitDirectory {
return URL(filePath: "\(gitDirectory)/\(filePath)")
}
return URL(filePath: filePath)
}
}
private extension GitClient {
func version(includeCommitSha: Bool, gitDirectory: String?) async throws -> String {
@Dependency(\.gitClient) var gitClient
return try await gitClient.version(.init(
gitDirectory: gitDirectory,
style: .branch(commitSha: includeCommitSha)
)).description
}
}
private extension Configuration.PreRelease {
func preReleaseString(gitDirectory: String?) async throws -> PreReleaseString? {
guard let strategy else { return nil }
@Dependency(\.asyncShellClient) var asyncShellClient
@Dependency(\.gitClient) var gitClient
@Dependency(\.logger) var logger
var preReleaseString: String
var suffix = true
var allowsPrefix = true
switch strategy {
case let .branch(includeCommitSha: includeCommitSha):
logger.trace("Branch pre-release strategy, includeCommitSha: \(includeCommitSha).")
preReleaseString = try await gitClient.version(
includeCommitSha: includeCommitSha,
gitDirectory: gitDirectory
)
case let .command(arguments: arguments, allowPrefix: allowPrefix):
logger.trace("Command pre-release strategy, arguments: \(arguments).")
preReleaseString = try await asyncShellClient.background(.init(arguments))
allowsPrefix = allowPrefix ?? false
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(
gitDirectory: gitDirectory,
style: .tag(exactMatch: false)
)).description
}
if let prefix, allowsPrefix {
preReleaseString = "\(prefix)-\(preReleaseString)"
}
guard suffix else { return .semvar(preReleaseString) }
return .suffix(preReleaseString)
}
enum PreReleaseString: Sendable {
case suffix(String)
case semvar(String)
}
}
private extension Configuration.SemVar {
func applyingPreRelease(_ semvar: SemVar, _ gitDirectory: String?) async throws -> SemVar {
@Dependency(\.logger) var logger
logger.trace("Start apply pre-release to: \(semvar)")
guard let preReleaseStrategy = self.preRelease,
let preRelease = try await preReleaseStrategy.preReleaseString(gitDirectory: gitDirectory)
else {
logger.trace("No pre-release strategy, returning original semvar.")
return semvar
}
logger.trace("Pre-release string: \(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)
}
if let prefix = self.preRelease?.prefix {
var prefixString = prefix
if let preReleaseString = semvar.preRelease {
prefixString = "\(prefix)-\(preReleaseString)"
}
return semvar.applyingPreRelease(prefixString)
}
return semvar
}
}
}
enum ConfigurationParsingError: Error {
case targetNotFound
case pathOrModuleNotSet
case versionStrategyError(message: String)
case versionStrategyNotFound
}

View File

@@ -5,44 +5,35 @@ import GitClient
@_spi(Internal)
public extension FileClient {
func loadCurrentVersion(
url: URL,
gitDirectory: String?,
expectsBranch: Bool
) async throws -> CurrentVersionContainer.CurrentVersion? {
@Dependency(\.logger) var logger
switch expectsBranch {
case true:
let (string, usesOptionalType) = try await branch(file: url, gitDirectory: gitDirectory)
logger.debug("Loaded branch: \(string)")
return .branch(string, usesOptionalType: usesOptionalType)
case false:
let (semvar, usesOptionalType) = try await semvar(file: url, gitDirectory: gitDirectory)
guard let semvar else { return nil }
logger.debug("Semvar: \(semvar)")
return .semvar(semvar, usesOptionalType: usesOptionalType)
}
}
// TODO: Make private.
func branch(
file: URL,
gitDirectory: String?
) async throws -> (string: String, usesOptionalType: Bool) {
let (string, usesOptionalType) = try await getVersionString(fileUrl: file, gitDirectory: gitDirectory)
return (string, usesOptionalType)
gitDirectory: String?,
requireExistingFile: Bool
) async throws -> (string: String, usesOptionalType: Bool)? {
let loaded = try? await getVersionString(fileUrl: file, gitDirectory: gitDirectory)
guard let loaded else {
if requireExistingFile {
throw CliClientError.fileDoesNotExist(path: file.cleanFilePath)
}
return nil
}
return (loaded.0, loaded.1)
}
// TODO: Make private.
func semvar(
file: URL,
gitDirectory: String?
) async throws -> (semVar: SemVar?, usesOptionalType: Bool) {
let (string, usesOptionalType) = try await getVersionString(fileUrl: file, gitDirectory: gitDirectory)
let semvar = SemVar(string: string)
return (semvar, usesOptionalType)
gitDirectory: String?,
requireExistingFile: Bool
) async throws -> (semVar: SemVar?, usesOptionalType: Bool)? {
let loaded = try? await getVersionString(fileUrl: file, gitDirectory: gitDirectory)
guard let loaded else {
if requireExistingFile {
throw CliClientError.fileDoesNotExist(path: file.cleanFilePath)
}
return nil
}
let semvar = SemVar(string: loaded.0)
return (semvar, loaded.1)
}
private func getVersionString(

View File

@@ -0,0 +1,90 @@
import ConfigurationClient
import Foundation
import GitClient
import LoggingExtensions
import ShellClient
extension SemVar {
static func nextVersion(
configuration: Configuration.SemVar,
projectDirectory: String?
) async throws -> Self? {
@Dependency(\.asyncShellClient) var asyncShellClient
@Dependency(\.gitClient) var gitClient
@Dependency(\.logger) var logger
guard let strategy = configuration.strategy else { return nil }
let semvarString: String
switch strategy {
case let .gitTag(exactMatch: exactMatch):
logger.trace("Loading semvar gitTag strategy...")
semvarString = try await gitClient.version(.init(
gitDirectory: projectDirectory,
style: .tag(exactMatch: exactMatch ?? false)
)).description
case let .command(arguments: arguments):
logger.trace("Loading semvar custom command strategy: \(arguments)")
semvarString = try await asyncShellClient.background(.init(arguments))
}
var preReleaseString: String?
if let preRelease = configuration.preRelease,
configuration.allowPreRelease ?? true
{
preReleaseString = try await preRelease.get(projectDirectory: projectDirectory)
}
let semvar = SemVar(string: semvarString)
if let preReleaseString {
return semvar?.applyingPreRelease(preReleaseString)
}
return semvar
}
}
private extension Configuration.PreRelease {
func get(projectDirectory: String?) async throws -> String? {
@Dependency(\.asyncShellClient) var asyncShellClient
@Dependency(\.gitClient) var gitClient
@Dependency(\.logger) var logger
var allowsPrefix = true
var preReleaseString: String
guard let strategy else { return nil }
switch strategy {
case let .branch(includeCommitSha: includeCommitSha):
logger.trace("Loading pre-relase branch strategy...")
preReleaseString = try await gitClient.version(.init(
gitDirectory: projectDirectory,
style: .branch(commitSha: includeCommitSha)
)).description
case .gitTag:
logger.trace("Loading pre-relase gitTag strategy...")
preReleaseString = try await gitClient.version(.init(
gitDirectory: projectDirectory,
style: .tag(exactMatch: false)
)).description
case let .command(arguments: arguments, allowPrefix: allowPrefix):
logger.trace("Loading pre-relase custom command strategy...")
allowsPrefix = allowPrefix ?? false
preReleaseString = try await asyncShellClient.background(.init(arguments))
}
if let prefix, allowsPrefix {
preReleaseString = "\(prefix)-\(preReleaseString)"
}
logger.trace("Pre-release string: \(preReleaseString)")
return preReleaseString
}
}

View File

@@ -0,0 +1,182 @@
import ConfigurationClient
import CustomDump
import Dependencies
import FileClient
import Foundation
import LoggingExtensions
enum VersionContainer: Sendable {
case branch(CurrentVersionContainer<String>)
case semvar(CurrentVersionContainer<SemVar>)
static func load(
projectDirectory: String?,
strategy: Configuration.VersionStrategy,
url: URL
) async throws -> Self {
switch strategy {
case let .branch(includeCommitSha: includeCommitSha):
return try await .branch(.load(
branch: .init(includeCommitSha: includeCommitSha),
gitDirectory: projectDirectory,
url: url
))
case .semvar:
return try await .semvar(.load(
semvar: strategy.semvar!,
gitDirectory: projectDirectory,
url: url
))
}
}
}
// TODO: Add a precedence field for which version to prefer, should also be specified in
// configuration.
struct CurrentVersionContainer<Version> {
let targetUrl: URL
let usesOptionalType: Bool
let loadedVersion: Version?
let precedence: Configuration.SemVar.Precedence?
let strategyVersion: Version?
}
extension CurrentVersionContainer: Equatable where Version: Equatable {
var hasChanges: Bool {
switch (loadedVersion, strategyVersion) {
case (.none, .none):
return false
case (.some, .none),
(.none, .some):
return true
case let (.some(loaded), .some(next)):
return loaded == next
}
}
}
extension CurrentVersionContainer: Sendable where Version: Sendable {}
extension CurrentVersionContainer where Version == String {
static func load(
branch: Configuration.Branch,
gitDirectory: String?,
url: URL
) async throws -> Self {
@Dependency(\.fileClient) var fileClient
@Dependency(\.gitClient) var gitClient
let loaded = try await fileClient.branch(
file: url,
gitDirectory: gitDirectory,
requireExistingFile: false
)
let next = try await gitClient.version(.init(
gitDirectory: gitDirectory,
style: .branch(commitSha: branch.includeCommitSha)
))
return .init(
targetUrl: url,
usesOptionalType: loaded?.1 ?? true,
loadedVersion: loaded?.0,
precedence: nil,
strategyVersion: next.description
)
}
var versionString: String? {
loadedVersion ?? strategyVersion
}
}
extension CurrentVersionContainer where Version == SemVar {
// TODO: Update to use precedence and not fetch `nextVersion` if we loaded a file version.
static func load(semvar: Configuration.SemVar, gitDirectory: String?, url: URL) async throws -> Self {
@Dependency(\.fileClient) var fileClient
@Dependency(\.logger) var logger
logger.trace("Begin loading semvar from: \(url.cleanFilePath)")
async let (loaded, usesOptionalType) = try await loadCurrentVersion(
semvar: semvar,
gitDirectory: gitDirectory,
url: url
)
async let next = try await loadNextVersion(semvar: semvar, projectDirectory: gitDirectory)
return try await .init(
targetUrl: url,
usesOptionalType: usesOptionalType,
loadedVersion: loaded,
precedence: semvar.precedence,
strategyVersion: next
)
}
static func loadCurrentVersion(
semvar: Configuration.SemVar,
gitDirectory: String?,
url: URL
) async throws -> (SemVar?, Bool) {
@Dependency(\.fileClient) var fileClient
@Dependency(\.logger) var logger
logger.trace("Begin loading current version from: \(url.cleanFilePath)")
let loadedOptional = try await fileClient.semvar(
file: url,
gitDirectory: gitDirectory,
requireExistingFile: semvar.requireExistingFile ?? false
)
guard let loadedStrong = loadedOptional else {
if semvar.requireExistingFile ?? false {
throw CliClientError.semVarNotFound(message: "Required by configuration's 'requireExistingFile' variable.")
}
return (nil, true)
}
let (loaded, usesOptionalType) = loadedStrong
logger.dump(loaded) { "Loaded version:\n\($0)" }
return (loaded, usesOptionalType)
}
static func loadNextVersion(semvar: Configuration.SemVar, projectDirectory: String?) async throws -> SemVar? {
@Dependency(\.logger) var logger
let next = try await SemVar.nextVersion(
configuration: semvar,
projectDirectory: projectDirectory
)
logger.dump(next) { "Next version:\n\($0)" }
return next
}
func versionString(withPreRelease: Bool) -> String? {
let version: SemVar?
switch precedence ?? .default {
case .file:
version = loadedVersion ?? strategyVersion
case .strategy:
version = strategyVersion ?? loadedVersion
}
return version?.versionString(withPreReleaseTag: withPreRelease)
}
// TODO: Move to where `bump` is declared and make fileprivate.
func withUpdateNextVersion(_ next: SemVar) -> Self {
.init(
targetUrl: targetUrl,
usesOptionalType: usesOptionalType,
loadedVersion: loadedVersion,
precedence: .strategy, // make sure to use the next version, since it was specified, as this is called from `bump`.
strategyVersion: next
)
}
}

View File

@@ -251,6 +251,8 @@ public extension Configuration {
public let allowPreRelease: Bool?
public let precedence: Precedence?
/// Optional pre-releas suffix strategy.
public let preRelease: PreRelease?
@@ -264,12 +266,14 @@ public extension Configuration {
public init(
allowPreRelease: Bool? = true,
precedence: Precedence? = .default,
preRelease: PreRelease? = nil,
requireExistingFile: Bool? = false,
requireExistingSemVar: Bool? = false,
strategy: Strategy? = nil
) {
self.allowPreRelease = allowPreRelease
self.precedence = precedence
self.preRelease = preRelease
self.requireExistingFile = requireExistingFile
self.requireExistingSemVar = requireExistingSemVar
@@ -281,6 +285,13 @@ public extension Configuration {
case gitTag(exactMatch: Bool? = false)
}
public enum Precedence: String, Codable, Equatable, Sendable {
case file
case strategy
public static var `default`: Self { .file }
}
}
}

View File

@@ -1,4 +1,6 @@
import CustomDump
import Dependencies
import Logging
import ShellClient
public struct LoggingOptions: Equatable, Sendable {
@@ -26,3 +28,15 @@ public struct LoggingOptions: Equatable, Sendable {
}
}
}
public extension Logger {
func dump<T>(
_ type: T,
level: Level = .trace,
buildMessage: @escaping (String) -> String = { $0 }
) {
var message = ""
customDump(type, to: &message)
log(level: level, "\(buildMessage(message))")
}
}

View File

@@ -15,8 +15,10 @@ struct CliClientTests {
arguments: TestArguments.testCases
)
func testBuild(target: String) async throws {
let template = Template.build("1.0.0")
try await run {
$0.fileClient.fileExists = { _ in true }
$0.fileClient.read = { @Sendable _ in template }
} operation: {
@Dependency(\.cliClient) var client
let output = try await client.build(.testOptions(

1
gum Normal file
View File

@@ -0,0 +1 @@
foo,bar,baz table