feat: Updates internal version container used to derive next version in cli-client.
This commit is contained in:
@@ -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")
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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: ¤tVersionString)
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
90
Sources/CliClient/Internal/Semvar+nextVersion.swift
Normal file
90
Sources/CliClient/Internal/Semvar+nextVersion.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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))")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user