Compare commits

...

3 Commits

12 changed files with 454 additions and 398 deletions

View File

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

View File

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

View File

@@ -4,25 +4,29 @@ import Dependencies
import FileClient import FileClient
import Foundation import Foundation
import GitClient import GitClient
import LoggingExtensions
@_spi(Internal) extension CliClient.SharedOptions {
public extension CliClient.SharedOptions {
/// All cli-client calls should run through this, it set's up logging, /// All cli-client calls should run through this, it set's up logging,
/// loads configuration, and generates the current version based on the /// loads configuration, and generates the current version based on the
/// configuration. /// configuration.
@discardableResult @discardableResult
func run( func run(
_ operation: (CurrentVersionContainer) async throws -> Void _ operation: (VersionContainer) async throws -> Void
) async rethrows -> String { ) async rethrows -> String {
try await loggingOptions.withLogger { try await loggingOptions.withLogger {
// Load the default configuration, if it exists. // Load the default configuration, if it exists.
try await withMergedConfiguration { configuration in try await withMergedConfiguration { configuration in
@Dependency(\.logger) var logger @Dependency(\.logger) var logger
var configurationString = "" guard let strategy = configuration.strategy else {
customDump(configuration, to: &configurationString) throw CliClientError.strategyNotFound(configuration: configuration)
logger.trace("\nConfiguration: \(configurationString)") }
logger.dump(configuration, level: .trace) {
"\nConfiguration: \($0)"
}
// This will fail if the target url is not set properly. // This will fail if the target url is not set properly.
let targetUrl = try configuration.targetUrl(gitDirectory: projectDirectory) 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. // Perform the operation, which generates the new version and writes it.
try await operation( try await operation(
configuration.currentVersion( .load(projectDirectory: projectDirectory, strategy: strategy, url: targetUrl)
targetUrl: targetUrl,
gitDirectory: projectDirectory
)
) )
// Return the file path we wrote the version to. // Return the file path we wrote the version to.
@@ -54,10 +55,9 @@ public extension CliClient.SharedOptions {
logger.trace("Merging branch strategy.") logger.trace("Merging branch strategy.")
// strategy = .branch(branch) // strategy = .branch(branch)
} else if let semvar = configurationToMerge?.strategy?.semvar { } else if let semvar = configurationToMerge?.strategy?.semvar {
logger.trace("Merging semvar strategy.") logger.dump(semvar, level: .trace) {
var semvarString = "" "Merging semvar strategy:\n\($0)"
customDump(semvar, to: &semvarString) }
logger.trace("\(semvarString)")
} }
return try await configurationClient.withConfiguration( return try await configurationClient.withConfiguration(
@@ -75,63 +75,52 @@ 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.info("\n\(string)\n")
} }
} }
func write(_ currentVersion: CurrentVersionContainer) async throws { func write(_ currentVersion: VersionContainer) async throws {
@Dependency(\.logger) var logger @Dependency(\.logger) var logger
logger.trace("Begin writing version.")
let version = try currentVersion.version.string(allowPreReleaseTag: allowPreReleaseTag) let hasChanges: Bool
if !dryRun { let targetUrl: URL
logger.debug("Version: \(version)") let usesOptionalType: Bool
} else { let versionString: String?
logger.info("Version: \(version)")
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)") logger.trace("Template string: \(template)")
try await write(template, to: currentVersion.targetUrl) try await write(template, to: 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)
}
}
} }
} }
@@ -152,30 +141,30 @@ extension CliClient.SharedOptions {
@Dependency(\.logger) var logger @Dependency(\.logger) var logger
switch container.version { switch container {
case .string: // When we did not parse a semvar, just write whatever we parsed for the current version. 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.") logger.debug("Failed to parse semvar, but got current version string.")
try await write(container) try await write(container)
case let .semvar(semvar, usesOptionalType: usesOptionalType, hasChanges: hasChanges): case let .semvar(semvar):
logger.debug("Semvar prior to bumping: \(semvar)")
let bumped = semvar.bump(type)
let version = bumped.versionString(withPreReleaseTag: allowPreReleaseTag)
guard bumped != semvar || hasChanges else { let version: SemVar?
logger.debug("No change, skipping.")
return switch semvar.precedence ?? .default {
case .file:
version = semvar.loadedVersion ?? semvar.strategyVersion
case .strategy:
version = semvar.strategyVersion ?? semvar.loadedVersion
} }
logger.debug("Bumped version: \(version)") // let version = semvar.loadedVersion ?? semvar.nextVersion
guard let version else {
if dryRun { throw CliClientError.semVarNotFound(message: "Failed to parse a valid semvar to bump.")
logger.info("Version: \(version)")
return
} }
logger.dump(version, level: .debug) { "Version prior to bumping:\n\($0)" }
let template = usesOptionalType ? Template.optional(version) : Template.build(version) let bumped = version.bump(type)
try await write(template, to: container.targetUrl) 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) @_spi(Internal)
public extension FileClient { 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( func branch(
file: URL, file: URL,
gitDirectory: String? gitDirectory: String?,
) async throws -> (string: String, usesOptionalType: Bool) { requireExistingFile: Bool
let (string, usesOptionalType) = try await getVersionString(fileUrl: file, gitDirectory: gitDirectory) ) async throws -> (string: String, usesOptionalType: Bool)? {
return (string, usesOptionalType) 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( func semvar(
file: URL, file: URL,
gitDirectory: String? gitDirectory: String?,
) async throws -> (semVar: SemVar?, usesOptionalType: Bool) { requireExistingFile: Bool
let (string, usesOptionalType) = try await getVersionString(fileUrl: file, gitDirectory: gitDirectory) ) async throws -> (semVar: SemVar?, usesOptionalType: Bool)? {
let semvar = SemVar(string: string) let loaded = try? await getVersionString(fileUrl: file, gitDirectory: gitDirectory)
return (semvar, usesOptionalType) 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( 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 allowPreRelease: Bool?
public let precedence: Precedence?
/// Optional pre-releas suffix strategy. /// Optional pre-releas suffix strategy.
public let preRelease: PreRelease? public let preRelease: PreRelease?
@@ -264,12 +266,14 @@ public extension Configuration {
public init( public init(
allowPreRelease: Bool? = true, allowPreRelease: Bool? = true,
precedence: Precedence? = .default,
preRelease: PreRelease? = nil, preRelease: PreRelease? = nil,
requireExistingFile: Bool? = false, requireExistingFile: Bool? = false,
requireExistingSemVar: Bool? = false, requireExistingSemVar: Bool? = false,
strategy: Strategy? = nil strategy: Strategy? = nil
) { ) {
self.allowPreRelease = allowPreRelease self.allowPreRelease = allowPreRelease
self.precedence = precedence
self.preRelease = preRelease self.preRelease = preRelease
self.requireExistingFile = requireExistingFile self.requireExistingFile = requireExistingFile
self.requireExistingSemVar = requireExistingSemVar self.requireExistingSemVar = requireExistingSemVar
@@ -281,6 +285,13 @@ public extension Configuration {
case gitTag(exactMatch: Bool? = false) 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 Dependencies
import Logging
import ShellClient import ShellClient
public struct LoggingOptions: Equatable, Sendable { 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 arguments: TestArguments.testCases
) )
func testBuild(target: String) async throws { func testBuild(target: String) async throws {
let template = Template.build("1.0.0")
try await run { try await run {
$0.fileClient.fileExists = { _ in true } $0.fileClient.fileExists = { _ in true }
$0.fileClient.read = { @Sendable _ in template }
} operation: { } operation: {
@Dependency(\.cliClient) var client @Dependency(\.cliClient) var client
let output = try await client.build(.testOptions( let output = try await client.build(.testOptions(

1
gum Normal file
View File

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