feat: Updates internal version container used to derive next version in cli-client.

This commit is contained in:
2024-12-28 17:05:33 -05:00
parent 6fe459c39e
commit 9631c62ee3
9 changed files with 331 additions and 415 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,24 +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)
} }
} }
@@ -113,30 +141,20 @@ 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)") // FIX: Fix with an option for precedence.
let bumped = semvar.bump(type) let version = semvar.loadedVersion ?? semvar.nextVersion
let version = bumped.versionString(withPreReleaseTag: allowPreReleaseTag) guard let version else {
throw CliClientError.semVarNotFound(message: "Failed to parse a valid semvar to bump.")
guard bumped != semvar || hasChanges else {
logger.debug("No change, skipping.")
return
} }
logger.debug("Semvar prior to bumping: \(version)")
logger.debug("Bumped version: \(version)") let bumped = version.bump(type)
try await write(.semvar(semvar.withUpdateNextVersion(bumped)))
if dryRun {
logger.info("Version: \(version)")
return
}
let template = usesOptionalType ? Template.optional(version) : Template.build(version)
try await write(template, to: container.targetUrl)
} }
} }
} }

View File

@@ -12,174 +12,6 @@ extension Configuration {
} }
return try target.url(gitDirectory: gitDirectory) 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
)
}
}
extension Configuration.VersionStrategy {
func loadNextVersion(gitDirectory: String?) async throws -> CurrentVersionContainer.Version {
@Dependency(\.gitClient) var gitClient
@Dependency(\.logger) var logger
switch self {
case let .branch(includeCommitSha: includeCommitSha):
logger.trace("Loading next version for branch strategy...")
let next = try await gitClient.version(includeCommitSha: includeCommitSha, gitDirectory: gitDirectory)
logger.trace("Next version: \(next)")
return .string(next)
case .semvar:
logger.trace("Loading next version for semvar strategy...")
let semvar = semvar!
guard let strategy = semvar.strategy else {
// TODO: Error here.
fatalError()
}
let next = try await strategy.getSemvar(gitDirectory: gitDirectory)
var nextString = ""
customDump(next, to: &nextString)
logger.trace("Next version:\n\(nextString)")
// TODO: Need to check for pre-release and load it here.
// TODO: Fix optional type / has changes.
return .semvar(next, usesOptionalType: true, 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 { private extension Configuration.Target {
@@ -217,101 +49,6 @@ private extension Configuration.Target {
} }
} }
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 {
// TODO: Move this to SemVar, not 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 { enum ConfigurationParsingError: Error {
case targetNotFound case targetNotFound
case pathOrModuleNotSet case pathOrModuleNotSet

View File

@@ -3,101 +3,160 @@ import CustomDump
import Dependencies import Dependencies
import FileClient import FileClient
import Foundation import Foundation
import LoggingExtensions
// TODO: Add optional property for currentVersion (loaded version from file) enum VersionContainer: Sendable {
// and rename version to nextVersion. case branch(CurrentVersionContainer2<String>)
case semvar(CurrentVersionContainer2<SemVar>)
/// An internal type that holds onto the loaded version from a file (if found), static func load(
/// the computed next version, and the target file url. projectDirectory: String?,
/// strategy: Configuration.VersionStrategy,
@_spi(Internal) url: URL
public struct CurrentVersionContainer: Sendable { ) async throws -> Self {
switch strategy {
let targetUrl: URL case let .branch(includeCommitSha: includeCommitSha):
let currentVersion: CurrentVersion? return try await .branch(.load(
let version: Version branch: .init(includeCommitSha: includeCommitSha),
gitDirectory: projectDirectory,
// TODO: Derive from current version. url: url
var usesOptionalType: Bool { ))
switch version { case .semvar:
case .string: return false return try await .semvar(.load(
case let .semvar(_, usesOptionalType, _): return usesOptionalType semvar: strategy.semvar!,
} gitDirectory: projectDirectory,
} url: url
))
var hasChanges: Bool {
guard let currentVersion else { return false }
switch (currentVersion, version) {
case let (.branch(currentString, _), .string(nextString)):
return currentString == nextString
case let (.semvar(currentSemvar, _), .semvar(nextSemvar, _, _)):
return currentSemvar == nextSemvar
// TODO: What to do with mis-matched values.
case (.branch, .semvar),
(.semvar, .string):
return true
}
}
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)
}
} }
} }
} }
extension CurrentVersionContainer { struct CurrentVersionContainer2<Version> {
let targetUrl: URL
let usesOptionalType: Bool
let loadedVersion: Version?
// TODO: Rename to strategyVersion
let nextVersion: Version?
}
extension CurrentVersionContainer2: Equatable where Version: Equatable {
var hasChanges: Bool {
switch (loadedVersion, nextVersion) {
case (.none, .none):
return false
case (.some, .none),
(.none, .some):
return true
case let (.some(loaded), .some(next)):
return loaded == next
}
}
}
extension CurrentVersionContainer2: Sendable where Version: Sendable {}
extension CurrentVersionContainer2 where Version == String {
static func load( static func load(
configuration: Configuration, branch: Configuration.Branch,
sharedOptions: CliClient.SharedOptions gitDirectory: String?,
url: URL
) async throws -> Self { ) 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,
nextVersion: next.description
)
}
var versionString: String? {
loadedVersion ?? nextVersion
}
}
extension CurrentVersionContainer2 where Version == SemVar {
static func load(semvar: Configuration.SemVar, gitDirectory: String?, url: URL) async throws -> Self {
@Dependency(\.fileClient) var fileClient @Dependency(\.fileClient) var fileClient
@Dependency(\.logger) var logger @Dependency(\.logger) var logger
let targetUrl = try configuration.targetUrl(gitDirectory: sharedOptions.projectDirectory) logger.trace("Begin loading semvar from: \(url.cleanFilePath)")
logger.trace("Begin loading current version from: \(targetUrl.cleanFilePath)")
let currentVersion = try await fileClient.loadCurrentVersion( async let (loaded, usesOptionalType) = try await loadCurrentVersion(
url: targetUrl, semvar: semvar,
gitDirectory: sharedOptions.projectDirectory, gitDirectory: gitDirectory,
expectsBranch: configuration.strategy?.branch != nil url: url
) )
var currentVersionString = "" async let next = try await loadNextVersion(semvar: semvar, projectDirectory: gitDirectory)
customDump(currentVersion, to: &currentVersionString)
logger.trace("Loaded current version:\n\(currentVersionString)")
if configuration.strategy?.semvar?.requireExistingFile == true { return try await .init(
guard currentVersion != nil else { targetUrl: url,
// TODO: Better error. usesOptionalType: usesOptionalType,
throw CliClientError.semVarNotFound loadedVersion: loaded,
} nextVersion: next
)
} }
// Check that there's a valid strategy to get the next version. static func loadCurrentVersion(
guard let strategy = configuration.strategy else { semvar: Configuration.SemVar,
// TODO: Return without a next version here when nextVersion is optional. gitDirectory: String?,
fatalError() 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)
} }
// TODO: make optional? let (loaded, usesOptionalType) = loadedStrong
let next = try await strategy.loadNextVersion(gitDirectory: sharedOptions.projectDirectory)
fatalError() 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? {
nextVersion?.versionString(withPreReleaseTag: withPreRelease)
?? loadedVersion?.versionString(withPreReleaseTag: withPreRelease)
}
func withUpdateNextVersion(_ next: SemVar) -> Self {
.init(targetUrl: targetUrl, usesOptionalType: usesOptionalType, loadedVersion: loadedVersion, nextVersion: next)
} }
} }

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

@@ -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(