Compare commits
8 Commits
04bfd4a6ae
...
0.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
47bad744d5
|
|||
|
2650bdc670
|
|||
|
a0f8611a76
|
|||
|
86a344fa9f
|
|||
|
56359f3488
|
|||
|
fbb4a22e98
|
|||
|
3cfd882f38
|
|||
|
a885e3dfa3
|
@@ -1,14 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"target" : {
|
||||||
|
"module" : { "name" : "bump-version" }
|
||||||
|
},
|
||||||
"strategy" : {
|
"strategy" : {
|
||||||
"semvar" : {
|
"semvar" : {
|
||||||
"preRelease" : {
|
"preRelease" : { "strategy": { "gitTag" : {} } },
|
||||||
"strategy" : { "gitTag" : {} }
|
"strategy" : { "gitTag": { "exactMatch": false } }
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"target" : {
|
|
||||||
"module" : {
|
|
||||||
"name" : "bump-version"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "6ab0a9c883cfa1490d249a344074ad27369033fab78e1a90272ef07339a8c0ab",
|
"originHash" : "3640b52c8069b868611efbfbd9b7545872526454802225747f7cd878062df1db",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "combine-schedulers",
|
"identity" : "combine-schedulers",
|
||||||
@@ -28,6 +28,15 @@
|
|||||||
"version" : "1.5.0"
|
"version" : "1.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-cli-doc",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/m-housh/swift-cli-doc.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "bbace73d974fd3e6985461431692bea773c7c5d8",
|
||||||
|
"version" : "0.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "swift-clocks",
|
"identity" : "swift-clocks",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ let package = Package(
|
|||||||
.library(name: "ConfigurationClient", targets: ["ConfigurationClient"]),
|
.library(name: "ConfigurationClient", targets: ["ConfigurationClient"]),
|
||||||
.library(name: "FileClient", targets: ["FileClient"]),
|
.library(name: "FileClient", targets: ["FileClient"]),
|
||||||
.library(name: "GitClient", targets: ["GitClient"]),
|
.library(name: "GitClient", targets: ["GitClient"]),
|
||||||
|
.library(name: "LoggingExtensions", targets: ["LoggingExtensions"]),
|
||||||
.plugin(name: "BuildWithVersionPlugin", targets: ["BuildWithVersionPlugin"]),
|
.plugin(name: "BuildWithVersionPlugin", targets: ["BuildWithVersionPlugin"]),
|
||||||
.plugin(name: "GenerateVersionPlugin", targets: ["GenerateVersionPlugin"]),
|
.plugin(name: "GenerateVersionPlugin", targets: ["GenerateVersionPlugin"]),
|
||||||
.plugin(name: "UpdateVersionPlugin", targets: ["UpdateVersionPlugin"])
|
.plugin(name: "UpdateVersionPlugin", targets: ["UpdateVersionPlugin"])
|
||||||
@@ -20,6 +21,7 @@ let package = Package(
|
|||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://github.com/pointfreeco/swift-dependencies.git", from: "1.6.2"),
|
.package(url: "https://github.com/pointfreeco/swift-dependencies.git", from: "1.6.2"),
|
||||||
.package(url: "https://github.com/m-housh/swift-shell-client.git", from: "0.2.2"),
|
.package(url: "https://github.com/m-housh/swift-shell-client.git", from: "0.2.2"),
|
||||||
|
.package(url: "https://github.com/m-housh/swift-cli-doc.git", from: "0.2.1"),
|
||||||
.package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.0.0"),
|
.package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.0.0"),
|
||||||
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.0"),
|
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.0"),
|
||||||
.package(url: "https://github.com/apple/swift-log.git", from: "1.6.2"),
|
.package(url: "https://github.com/apple/swift-log.git", from: "1.6.2"),
|
||||||
@@ -31,7 +33,8 @@ let package = Package(
|
|||||||
dependencies: [
|
dependencies: [
|
||||||
"CliClient",
|
"CliClient",
|
||||||
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
||||||
.product(name: "CustomDump", package: "swift-custom-dump")
|
.product(name: "CustomDump", package: "swift-custom-dump"),
|
||||||
|
.product(name: "CliDoc", package: "swift-cli-doc")
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
.target(
|
.target(
|
||||||
@@ -40,7 +43,9 @@ let package = Package(
|
|||||||
"ConfigurationClient",
|
"ConfigurationClient",
|
||||||
"FileClient",
|
"FileClient",
|
||||||
"GitClient",
|
"GitClient",
|
||||||
.product(name: "Logging", package: "swift-log")
|
"LoggingExtensions",
|
||||||
|
.product(name: "Logging", package: "swift-log"),
|
||||||
|
.product(name: "CustomDump", package: "swift-custom-dump")
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
@@ -80,6 +85,13 @@ let package = Package(
|
|||||||
name: "GitClientTests",
|
name: "GitClientTests",
|
||||||
dependencies: ["GitClient"]
|
dependencies: ["GitClient"]
|
||||||
),
|
),
|
||||||
|
.target(
|
||||||
|
name: "LoggingExtensions",
|
||||||
|
dependencies: [
|
||||||
|
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||||
|
.product(name: "ShellClient", package: "swift-shell-client")
|
||||||
|
]
|
||||||
|
),
|
||||||
.target(name: "TestSupport"),
|
.target(name: "TestSupport"),
|
||||||
.plugin(
|
.plugin(
|
||||||
name: "BuildWithVersionPlugin",
|
name: "BuildWithVersionPlugin",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import DependenciesMacros
|
|||||||
import FileClient
|
import FileClient
|
||||||
import Foundation
|
import Foundation
|
||||||
import GitClient
|
import GitClient
|
||||||
|
import LoggingExtensions
|
||||||
import ShellClient
|
import ShellClient
|
||||||
|
|
||||||
public extension DependencyValues {
|
public extension DependencyValues {
|
||||||
@@ -39,7 +40,7 @@ public struct CliClient: Sendable {
|
|||||||
let allowPreReleaseTag: Bool
|
let allowPreReleaseTag: Bool
|
||||||
let dryRun: Bool
|
let dryRun: Bool
|
||||||
let gitDirectory: String?
|
let gitDirectory: String?
|
||||||
let logLevel: Logger.Level
|
let loggingOptions: LoggingOptions
|
||||||
let target: Configuration.Target?
|
let target: Configuration.Target?
|
||||||
let branch: Configuration.Branch?
|
let branch: Configuration.Branch?
|
||||||
let semvar: Configuration.SemVar?
|
let semvar: Configuration.SemVar?
|
||||||
@@ -49,7 +50,7 @@ public struct CliClient: Sendable {
|
|||||||
allowPreReleaseTag: Bool = true,
|
allowPreReleaseTag: Bool = true,
|
||||||
dryRun: Bool = false,
|
dryRun: Bool = false,
|
||||||
gitDirectory: String? = nil,
|
gitDirectory: String? = nil,
|
||||||
logLevel: Logger.Level = .debug,
|
loggingOptions: LoggingOptions,
|
||||||
target: Configuration.Target? = nil,
|
target: Configuration.Target? = nil,
|
||||||
branch: Configuration.Branch? = nil,
|
branch: Configuration.Branch? = nil,
|
||||||
semvar: Configuration.SemVar? = nil,
|
semvar: Configuration.SemVar? = nil,
|
||||||
@@ -58,34 +59,12 @@ public struct CliClient: Sendable {
|
|||||||
self.allowPreReleaseTag = allowPreReleaseTag
|
self.allowPreReleaseTag = allowPreReleaseTag
|
||||||
self.dryRun = dryRun
|
self.dryRun = dryRun
|
||||||
self.gitDirectory = gitDirectory
|
self.gitDirectory = gitDirectory
|
||||||
self.logLevel = logLevel
|
self.loggingOptions = loggingOptions
|
||||||
self.target = target
|
self.target = target
|
||||||
self.branch = branch
|
self.branch = branch
|
||||||
self.semvar = semvar
|
self.semvar = semvar
|
||||||
self.configurationFile = configurationFile
|
self.configurationFile = configurationFile
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(
|
|
||||||
allowPreReleaseTag: Bool = true,
|
|
||||||
dryRun: Bool = false,
|
|
||||||
gitDirectory: String? = nil,
|
|
||||||
verbose: Int,
|
|
||||||
target: Configuration.Target? = nil,
|
|
||||||
branch: Configuration.Branch? = nil,
|
|
||||||
semvar: Configuration.SemVar? = nil,
|
|
||||||
configurationFile: String? = nil
|
|
||||||
) {
|
|
||||||
self.init(
|
|
||||||
allowPreReleaseTag: allowPreReleaseTag,
|
|
||||||
dryRun: dryRun,
|
|
||||||
gitDirectory: gitDirectory,
|
|
||||||
logLevel: .init(verbose: verbose),
|
|
||||||
target: target,
|
|
||||||
branch: branch,
|
|
||||||
semvar: semvar,
|
|
||||||
configurationFile: configurationFile
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -99,8 +78,10 @@ extension CliClient: DependencyKey {
|
|||||||
bump: { try await $1.bump($0) },
|
bump: { try await $1.bump($0) },
|
||||||
generate: { try await $0.generate() },
|
generate: { try await $0.generate() },
|
||||||
parsedConfiguration: { options in
|
parsedConfiguration: { options in
|
||||||
|
try await options.loggingOptions.withLogger {
|
||||||
try await options.withMergedConfiguration { $0 }
|
try await options.withMergedConfiguration { $0 }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ enum CliClientError: Error {
|
|||||||
case fileDoesNotExist(path: String)
|
case fileDoesNotExist(path: String)
|
||||||
case failedToParseVersionFile
|
case failedToParseVersionFile
|
||||||
case semVarNotFound
|
case semVarNotFound
|
||||||
|
case preReleaseParsingError(String)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import ConfigurationClient
|
import ConfigurationClient
|
||||||
|
import CustomDump
|
||||||
import Dependencies
|
import Dependencies
|
||||||
import FileClient
|
import FileClient
|
||||||
import Foundation
|
import Foundation
|
||||||
@@ -7,37 +8,21 @@ import GitClient
|
|||||||
@_spi(Internal)
|
@_spi(Internal)
|
||||||
public extension CliClient.SharedOptions {
|
public extension CliClient.SharedOptions {
|
||||||
|
|
||||||
// Merges any configuration set via the passed in options.
|
/// All cli-client calls should run through this, it set's up logging,
|
||||||
@discardableResult
|
/// loads configuration, and generates the current version based on the
|
||||||
func withMergedConfiguration<T>(
|
/// configuration.
|
||||||
operation: (Configuration) async throws -> T
|
|
||||||
) async throws -> T {
|
|
||||||
try await withConfiguration(path: configurationFile) { configuration in
|
|
||||||
var configuration = configuration
|
|
||||||
configuration = configuration.mergingTarget(target)
|
|
||||||
|
|
||||||
if configuration.strategy?.branch != nil, let branch {
|
|
||||||
configuration = configuration.mergingStrategy(.branch(branch))
|
|
||||||
} else if let semvar {
|
|
||||||
configuration = configuration.mergingStrategy(.semvar(semvar))
|
|
||||||
}
|
|
||||||
|
|
||||||
return try await operation(configuration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func run(
|
func run(
|
||||||
_ operation: (CurrentVersionContainer) async throws -> Void
|
_ operation: (CurrentVersionContainer) async throws -> Void
|
||||||
) async rethrows -> String {
|
) async rethrows -> String {
|
||||||
try await withDependencies {
|
try await loggingOptions.withLogger {
|
||||||
$0.logger.logLevel = logLevel
|
|
||||||
} operation: {
|
|
||||||
// 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
|
||||||
|
|
||||||
logger.debug("Configuration: \(configuration)")
|
var configurationString = ""
|
||||||
|
customDump(configuration, to: &configurationString)
|
||||||
|
logger.trace("\nConfiguration: \(configurationString)")
|
||||||
|
|
||||||
// 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: gitDirectory)
|
let targetUrl = try configuration.targetUrl(gitDirectory: gitDirectory)
|
||||||
@@ -57,6 +42,39 @@ public extension CliClient.SharedOptions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Merges any configuration set via the passed in options.
|
||||||
|
@discardableResult
|
||||||
|
func withMergedConfiguration<T>(
|
||||||
|
operation: (Configuration) async throws -> T
|
||||||
|
) async throws -> T {
|
||||||
|
@Dependency(\.configurationClient) var configurationClient
|
||||||
|
@Dependency(\.logger) var logger
|
||||||
|
|
||||||
|
var strategy: Configuration.VersionStrategy?
|
||||||
|
|
||||||
|
if let branch {
|
||||||
|
logger.trace("Merging branch strategy.")
|
||||||
|
strategy = .branch(branch)
|
||||||
|
} else if let semvar {
|
||||||
|
logger.trace("Merging semvar strategy.")
|
||||||
|
var semvarString = ""
|
||||||
|
customDump(semvar, to: &semvarString)
|
||||||
|
logger.trace("\(semvarString)")
|
||||||
|
strategy = .semvar(semvar)
|
||||||
|
}
|
||||||
|
|
||||||
|
let configuration = Configuration(
|
||||||
|
target: target,
|
||||||
|
strategy: strategy
|
||||||
|
)
|
||||||
|
|
||||||
|
return try await configurationClient.withConfiguration(
|
||||||
|
path: configurationFile,
|
||||||
|
merging: configuration,
|
||||||
|
operation: operation
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func write(_ string: String, to url: URL) async throws {
|
func write(_ string: String, to url: URL) async throws {
|
||||||
@Dependency(\.fileClient) var fileClient
|
@Dependency(\.fileClient) var fileClient
|
||||||
@Dependency(\.logger) var logger
|
@Dependency(\.logger) var logger
|
||||||
@@ -64,7 +82,7 @@ public extension CliClient.SharedOptions {
|
|||||||
try await fileClient.write(string: string, to: url)
|
try await fileClient.write(string: string, to: url)
|
||||||
} else {
|
} else {
|
||||||
logger.debug("Skipping, due to dry-run being passed.")
|
logger.debug("Skipping, due to dry-run being passed.")
|
||||||
logger.debug("\(string)")
|
logger.info("\n\(string)\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,20 +108,20 @@ public struct CurrentVersionContainer: Sendable {
|
|||||||
var usesOptionalType: Bool {
|
var usesOptionalType: Bool {
|
||||||
switch version {
|
switch version {
|
||||||
case .string: return false
|
case .string: return false
|
||||||
case let .semVar(_, usesOptionalType): return usesOptionalType
|
case let .semvar(_, usesOptionalType, _): return usesOptionalType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Version: Sendable {
|
public enum Version: Sendable {
|
||||||
case string(String)
|
case string(String)
|
||||||
case semVar(SemVar, usesOptionalType: Bool = true)
|
case semvar(SemVar, usesOptionalType: Bool = true, hasChanges: Bool)
|
||||||
|
|
||||||
func string(allowPreReleaseTag: Bool) throws -> String {
|
func string(allowPreReleaseTag: Bool) throws -> String {
|
||||||
switch self {
|
switch self {
|
||||||
case let .string(string):
|
case let .string(string):
|
||||||
return string
|
return string
|
||||||
case let .semVar(semVar, usesOptionalType: _):
|
case let .semvar(semvar, usesOptionalType: _, hasChanges: _):
|
||||||
return semVar.versionString(withPreReleaseTag: allowPreReleaseTag)
|
return semvar.versionString(withPreReleaseTag: allowPreReleaseTag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,14 +145,20 @@ extension CliClient.SharedOptions {
|
|||||||
@Dependency(\.logger) var logger
|
@Dependency(\.logger) var logger
|
||||||
|
|
||||||
switch container.version {
|
switch container.version {
|
||||||
case .string: // When we did not parse a semVar, just write whatever we parsed for the current version.
|
case .string: // 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):
|
case let .semvar(semvar, usesOptionalType: usesOptionalType, hasChanges: hasChanges):
|
||||||
logger.debug("Semvar prior to bumping: \(semVar)")
|
logger.debug("Semvar prior to bumping: \(semvar)")
|
||||||
let bumped = semVar.bump(type, preRelease: nil) // preRelease is already set on semVar.
|
let bumped = semvar.bump(type)
|
||||||
let version = bumped.versionString(withPreReleaseTag: allowPreReleaseTag)
|
let version = bumped.versionString(withPreReleaseTag: allowPreReleaseTag)
|
||||||
|
|
||||||
|
guard bumped != semvar || hasChanges else {
|
||||||
|
logger.debug("No change, skipping.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
logger.debug("Bumped version: \(version)")
|
logger.debug("Bumped version: \(version)")
|
||||||
let template = usesOptionalType ? Template.optional(version) : Template.build(version)
|
let template = usesOptionalType ? Template.optional(version) : Template.build(version)
|
||||||
try await write(template, to: container.targetUrl)
|
try await write(template, to: container.targetUrl)
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import ConfigurationClient
|
|
||||||
import Dependencies
|
|
||||||
import FileClient
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
extension Configuration {
|
|
||||||
|
|
||||||
func mergingTarget(_ otherTarget: Configuration.Target?) -> Self {
|
|
||||||
.init(
|
|
||||||
target: otherTarget ?? target,
|
|
||||||
strategy: strategy
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergingStrategy(_ otherStrategy: Configuration.VersionStrategy?) -> Self {
|
|
||||||
.init(
|
|
||||||
target: target,
|
|
||||||
strategy: strategy?.merging(otherStrategy)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Configuration.Branch {
|
|
||||||
func merging(_ other: Self?) -> Self {
|
|
||||||
return .init(includeCommitSha: other?.includeCommitSha ?? includeCommitSha)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Configuration.SemVar {
|
|
||||||
func merging(_ other: Self?) -> Self {
|
|
||||||
.init(
|
|
||||||
preRelease: other?.preRelease ?? preRelease,
|
|
||||||
requireExistingFile: other?.requireExistingFile ?? requireExistingFile,
|
|
||||||
requireExistingSemVar: other?.requireExistingSemVar ?? requireExistingSemVar
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Configuration.VersionStrategy {
|
|
||||||
func merging(_ other: Self?) -> Self {
|
|
||||||
guard let branch else {
|
|
||||||
guard let semvar else { return self }
|
|
||||||
return .semvar(semvar.merging(other?.semvar))
|
|
||||||
}
|
|
||||||
return .branch(branch.merging(other?.branch))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Configuration {
|
|
||||||
func merging(_ other: Self?) -> Self {
|
|
||||||
var output = self
|
|
||||||
output = output.mergingTarget(other?.target)
|
|
||||||
output = output.mergingStrategy(other?.strategy)
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
func withConfiguration<T>(
|
|
||||||
path: String?,
|
|
||||||
_ operation: (Configuration) async throws -> T
|
|
||||||
) async throws -> T {
|
|
||||||
@Dependency(\.configurationClient) var configurationClient
|
|
||||||
|
|
||||||
let configuration = try await configurationClient.findAndLoad(
|
|
||||||
path != nil ? URL(filePath: path!) : nil
|
|
||||||
)
|
|
||||||
|
|
||||||
return try await operation(configuration)
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,7 @@ import ConfigurationClient
|
|||||||
import Dependencies
|
import Dependencies
|
||||||
import Foundation
|
import Foundation
|
||||||
import GitClient
|
import GitClient
|
||||||
|
import ShellClient
|
||||||
|
|
||||||
extension Configuration {
|
extension Configuration {
|
||||||
func targetUrl(gitDirectory: String?) throws -> URL {
|
func targetUrl(gitDirectory: String?) throws -> URL {
|
||||||
@@ -58,26 +59,46 @@ extension Configuration.Target {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension GitClient {
|
extension GitClient {
|
||||||
func version(branch: Configuration.Branch, gitDirectory: String?) async throws -> String {
|
func version(includeCommitSha: Bool, gitDirectory: String?) async throws -> String {
|
||||||
@Dependency(\.gitClient) var gitClient
|
@Dependency(\.gitClient) var gitClient
|
||||||
|
|
||||||
return try await gitClient.version(.init(
|
return try await gitClient.version(.init(
|
||||||
gitDirectory: gitDirectory,
|
gitDirectory: gitDirectory,
|
||||||
style: .branch(commitSha: branch.includeCommitSha)
|
style: .branch(commitSha: includeCommitSha)
|
||||||
)).description
|
)).description
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Configuration.PreRelease {
|
extension Configuration.PreRelease {
|
||||||
|
|
||||||
func preReleaseString(gitDirectory: String?) async throws -> String {
|
// FIX: This needs to handle the pre-release type appropriatly.
|
||||||
|
func preReleaseString(gitDirectory: String?) async throws -> PreReleaseString? {
|
||||||
|
guard let strategy else { return nil }
|
||||||
|
|
||||||
|
@Dependency(\.asyncShellClient) var asyncShellClient
|
||||||
@Dependency(\.gitClient) var gitClient
|
@Dependency(\.gitClient) var gitClient
|
||||||
|
@Dependency(\.logger) var logger
|
||||||
|
|
||||||
let preReleaseString: String
|
var preReleaseString: String
|
||||||
|
var suffix = true
|
||||||
|
var allowsPrefix = true
|
||||||
|
|
||||||
if let branch = strategy?.branch {
|
switch strategy {
|
||||||
preReleaseString = try await gitClient.version(branch: branch, gitDirectory: gitDirectory)
|
case let .branch(includeCommitSha: includeCommitSha):
|
||||||
} else {
|
logger.trace("Branch pre-release strategy, includeCommitSha: \(includeCommitSha).")
|
||||||
|
preReleaseString = try await gitClient.version(
|
||||||
|
includeCommitSha: includeCommitSha,
|
||||||
|
gitDirectory: gitDirectory
|
||||||
|
)
|
||||||
|
case let .command(arguments: arguments):
|
||||||
|
logger.trace("Command pre-release strategy, arguments: \(arguments).")
|
||||||
|
// TODO: What to do with allows prefix? Need a configuration setting for commands.
|
||||||
|
preReleaseString = try await asyncShellClient.background(.init(arguments))
|
||||||
|
case .gitTag:
|
||||||
|
logger.trace("Git tag pre-release strategy.")
|
||||||
|
logger.trace("This will ignore any set prefix.")
|
||||||
|
suffix = false
|
||||||
|
allowsPrefix = false
|
||||||
preReleaseString = try await gitClient.version(.init(
|
preReleaseString = try await gitClient.version(.init(
|
||||||
gitDirectory: gitDirectory,
|
gitDirectory: gitDirectory,
|
||||||
style: .tag(exactMatch: false)
|
style: .tag(exactMatch: false)
|
||||||
@@ -85,27 +106,53 @@ extension Configuration.PreRelease {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let prefix {
|
if let prefix {
|
||||||
return "\(prefix)-\(preReleaseString)"
|
if allowsPrefix {
|
||||||
|
preReleaseString = "\(prefix)-\(preReleaseString)"
|
||||||
|
} else {
|
||||||
|
logger.warning("Found prefix, but pre-release strategy may not work properly, ignoring prefix.")
|
||||||
}
|
}
|
||||||
return preReleaseString
|
}
|
||||||
|
|
||||||
|
guard suffix else { return .semvar(preReleaseString) }
|
||||||
|
return .suffix(preReleaseString)
|
||||||
|
|
||||||
|
// return preReleaseString
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PreReleaseString: Sendable {
|
||||||
|
case suffix(String)
|
||||||
|
case semvar(String)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@_spi(Internal)
|
@_spi(Internal)
|
||||||
public extension Configuration.SemVar {
|
public extension Configuration.SemVar {
|
||||||
|
|
||||||
private func applyingPreRelease(_ semVar: SemVar, _ gitDirectory: String?) async throws -> SemVar {
|
private func applyingPreRelease(_ semvar: SemVar, _ gitDirectory: String?) async throws -> SemVar {
|
||||||
@Dependency(\.logger) var logger
|
@Dependency(\.logger) var logger
|
||||||
logger.trace("Start apply pre-release to: \(semVar)")
|
logger.trace("Start apply pre-release to: \(semvar)")
|
||||||
guard let preReleaseStrategy = self.preRelease else {
|
|
||||||
|
guard let preReleaseStrategy = self.preRelease,
|
||||||
|
let preRelease = try await preReleaseStrategy.preReleaseString(gitDirectory: gitDirectory)
|
||||||
|
else {
|
||||||
logger.trace("No pre-release strategy, returning original semvar.")
|
logger.trace("No pre-release strategy, returning original semvar.")
|
||||||
return semVar
|
return semvar
|
||||||
}
|
}
|
||||||
|
|
||||||
let preRelease = try await preReleaseStrategy.preReleaseString(gitDirectory: gitDirectory)
|
// let preRelease = try await preReleaseStrategy.preReleaseString(gitDirectory: gitDirectory)
|
||||||
logger.trace("Pre-release string: \(preRelease)")
|
logger.trace("Pre-release string: \(preRelease)")
|
||||||
|
|
||||||
return semVar.applyingPreRelease(preRelease)
|
switch preRelease {
|
||||||
|
case let .suffix(string):
|
||||||
|
return semvar.applyingPreRelease(string)
|
||||||
|
case let .semvar(string):
|
||||||
|
guard let semvar = SemVar(string: string) else {
|
||||||
|
throw CliClientError.preReleaseParsingError(string)
|
||||||
|
}
|
||||||
|
return semvar
|
||||||
|
}
|
||||||
|
|
||||||
|
// return semVar.applyingPreRelease(preRelease)
|
||||||
}
|
}
|
||||||
|
|
||||||
func currentVersion(file: URL, gitDirectory: String? = nil) async throws -> CurrentVersionContainer.Version {
|
func currentVersion(file: URL, gitDirectory: String? = nil) async throws -> CurrentVersionContainer.Version {
|
||||||
@@ -113,7 +160,7 @@ public extension Configuration.SemVar {
|
|||||||
@Dependency(\.gitClient) var gitClient
|
@Dependency(\.gitClient) var gitClient
|
||||||
@Dependency(\.logger) var logger
|
@Dependency(\.logger) var logger
|
||||||
|
|
||||||
let fileOutput = try? await fileClient.semVar(file: file, gitDirectory: gitDirectory)
|
let fileOutput = try? await fileClient.semvar(file: file, gitDirectory: gitDirectory)
|
||||||
var semVar = fileOutput?.semVar
|
var semVar = fileOutput?.semVar
|
||||||
|
|
||||||
logger.trace("file output semvar: \(String(describing: semVar))")
|
logger.trace("file output semvar: \(String(describing: semVar))")
|
||||||
@@ -122,13 +169,16 @@ public extension Configuration.SemVar {
|
|||||||
|
|
||||||
// We parsed a semvar from the existing file, use it.
|
// We parsed a semvar from the existing file, use it.
|
||||||
if semVar != nil {
|
if semVar != nil {
|
||||||
return try await .semVar(
|
let semvarWithPreRelease = try await applyingPreRelease(semVar!, gitDirectory)
|
||||||
applyingPreRelease(semVar!, gitDirectory),
|
|
||||||
usesOptionalType: usesOptionalType ?? false
|
return .semvar(
|
||||||
|
semvarWithPreRelease,
|
||||||
|
usesOptionalType: usesOptionalType ?? false,
|
||||||
|
hasChanges: semvarWithPreRelease != semVar
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if requireExistingFile {
|
if requireExistingFile == true {
|
||||||
logger.debug("Failed to parse existing file, and caller requires it.")
|
logger.debug("Failed to parse existing file, and caller requires it.")
|
||||||
throw CliClientError.fileDoesNotExist(path: file.cleanFilePath)
|
throw CliClientError.fileDoesNotExist(path: file.cleanFilePath)
|
||||||
}
|
}
|
||||||
@@ -142,22 +192,25 @@ public extension Configuration.SemVar {
|
|||||||
)).semVar
|
)).semVar
|
||||||
|
|
||||||
if semVar != nil {
|
if semVar != nil {
|
||||||
return try await .semVar(
|
let semvarWithPreRelease = try await applyingPreRelease(semVar!, gitDirectory)
|
||||||
applyingPreRelease(semVar!, gitDirectory),
|
return .semvar(
|
||||||
usesOptionalType: usesOptionalType ?? false
|
semvarWithPreRelease,
|
||||||
|
usesOptionalType: usesOptionalType ?? false,
|
||||||
|
hasChanges: semvarWithPreRelease != semVar
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if requireExistingSemVar {
|
if requireExistingSemVar == true {
|
||||||
logger.trace("Caller requires existing semvar and it was not found in file or git-tag.")
|
logger.trace("Caller requires existing semvar and it was not found in file or git-tag.")
|
||||||
throw CliClientError.semVarNotFound
|
throw CliClientError.semVarNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// Semvar doesn't exist, so create a new one.
|
// Semvar doesn't exist, so create a new one.
|
||||||
logger.trace("Generating new semvar.")
|
logger.trace("Generating new semvar.")
|
||||||
return try await .semVar(
|
return try await .semvar(
|
||||||
applyingPreRelease(.init(), gitDirectory),
|
applyingPreRelease(.init(), gitDirectory),
|
||||||
usesOptionalType: usesOptionalType ?? false
|
usesOptionalType: usesOptionalType ?? false,
|
||||||
|
hasChanges: true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,7 +234,7 @@ extension Configuration.VersionStrategy {
|
|||||||
return try await .init(
|
return try await .init(
|
||||||
targetUrl: targetUrl,
|
targetUrl: targetUrl,
|
||||||
version: .string(
|
version: .string(
|
||||||
gitClient.version(branch: branch, gitDirectory: gitDirectory)
|
gitClient.version(includeCommitSha: branch.includeCommitSha, gitDirectory: gitDirectory)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,17 +28,17 @@ public extension FileClient {
|
|||||||
logger.debug("Version line: \(versionLine)")
|
logger.debug("Version line: \(versionLine)")
|
||||||
|
|
||||||
let isOptional = versionLine.contains("String?")
|
let isOptional = versionLine.contains("String?")
|
||||||
logger.debug("Uses optional: \(isOptional)")
|
logger.trace("Uses optional: \(isOptional)")
|
||||||
|
|
||||||
let versionString = versionLine.split(separator: "let VERSION: \(isOptional ? "String?" : "String") = ").last
|
let versionString = versionLine.split(separator: "let VERSION: \(isOptional ? "String?" : "String") = ").last
|
||||||
guard let versionString else {
|
guard let versionString else {
|
||||||
throw CliClientError.failedToParseVersionFile
|
throw CliClientError.failedToParseVersionFile
|
||||||
}
|
}
|
||||||
logger.debug("Parsed version string: \(versionString)")
|
logger.trace("Parsed version string: \(versionString)")
|
||||||
return (String(versionString), isOptional)
|
return (String(versionString), isOptional)
|
||||||
}
|
}
|
||||||
|
|
||||||
func semVar(
|
func semvar(
|
||||||
file: URL,
|
file: URL,
|
||||||
gitDirectory: String?
|
gitDirectory: String?
|
||||||
) async throws -> (semVar: SemVar?, usesOptionalType: Bool) {
|
) async throws -> (semVar: SemVar?, usesOptionalType: Bool) {
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ public struct SemVar: CustomStringConvertible, Equatable, Sendable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Bumps the sem-var by the given option (major, minor, patch)
|
// Bumps the sem-var by the given option (major, minor, patch)
|
||||||
public func bump(_ option: CliClient.BumpOption, preRelease: String?) -> Self {
|
public func bump(_ option: CliClient.BumpOption, preRelease: String? = nil) -> Self {
|
||||||
switch option {
|
switch option {
|
||||||
case .major:
|
case .major:
|
||||||
return .init(
|
return .init(
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ public struct Template: Sendable {
|
|||||||
return """
|
return """
|
||||||
// Do not set this variable, it is set during the build process.
|
// Do not set this variable, it is set during the build process.
|
||||||
let VERSION: \(type.rawValue) = \(versionString)
|
let VERSION: \(type.rawValue) = \(versionString)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
71
Sources/ConfigurationClient/Configuration+merge.swift
Normal file
71
Sources/ConfigurationClient/Configuration+merge.swift
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import Dependencies
|
||||||
|
import FileClient
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
@_spi(Internal)
|
||||||
|
public extension Configuration {
|
||||||
|
|
||||||
|
func merging(_ other: Self?) -> Self {
|
||||||
|
mergingTarget(other?.target).mergingStrategy(other?.strategy)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func mergingTarget(_ otherTarget: Configuration.Target?) -> Self {
|
||||||
|
.init(
|
||||||
|
target: otherTarget ?? target,
|
||||||
|
strategy: strategy
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func mergingStrategy(_ otherStrategy: Configuration.VersionStrategy?) -> Self {
|
||||||
|
.init(
|
||||||
|
target: target,
|
||||||
|
strategy: strategy?.merging(otherStrategy)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@_spi(Internal)
|
||||||
|
public extension Configuration.PreRelease {
|
||||||
|
func merging(_ other: Self?) -> Self {
|
||||||
|
return .init(
|
||||||
|
prefix: other?.prefix ?? prefix,
|
||||||
|
strategy: other?.strategy ?? strategy
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@_spi(Internal)
|
||||||
|
public extension Configuration.Branch {
|
||||||
|
func merging(_ other: Self?) -> Self {
|
||||||
|
return .init(includeCommitSha: other?.includeCommitSha ?? includeCommitSha)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@_spi(Internal)
|
||||||
|
public extension Configuration.SemVar {
|
||||||
|
func merging(_ other: Self?) -> Self {
|
||||||
|
.init(
|
||||||
|
allowPreRelease: other?.allowPreRelease ?? allowPreRelease,
|
||||||
|
preRelease: preRelease == nil ? other?.preRelease : preRelease!.merging(other?.preRelease),
|
||||||
|
requireExistingFile: other?.requireExistingFile ?? requireExistingFile,
|
||||||
|
requireExistingSemVar: other?.requireExistingSemVar ?? requireExistingSemVar,
|
||||||
|
strategy: other?.strategy ?? strategy
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@_spi(Internal)
|
||||||
|
public extension Configuration.VersionStrategy {
|
||||||
|
func merging(_ other: Self?) -> Self {
|
||||||
|
guard let other else { return self }
|
||||||
|
|
||||||
|
switch other {
|
||||||
|
case .branch:
|
||||||
|
guard let branch else { return other }
|
||||||
|
return .branch(branch.merging(other.branch))
|
||||||
|
case .semvar:
|
||||||
|
guard let semvar else { return other }
|
||||||
|
return .semvar(semvar.merging(other.semvar))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import CustomDump
|
import CustomDump
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
// TODO: Add pre-release strategy that just bumps an integer.
|
||||||
|
|
||||||
/// Represents configuration that can be set via a file, generally in the root of the
|
/// Represents configuration that can be set via a file, generally in the root of the
|
||||||
/// project directory.
|
/// project directory.
|
||||||
///
|
///
|
||||||
@@ -26,7 +28,7 @@ public struct Configuration: Codable, Equatable, Sendable {
|
|||||||
public static func mock(module: String = "cli-version") -> Self {
|
public static func mock(module: String = "cli-version") -> Self {
|
||||||
.init(
|
.init(
|
||||||
target: .init(module: .init(module)),
|
target: .init(module: .init(module)),
|
||||||
strategy: .semvar(.init())
|
strategy: .semvar(.init(strategy: .gitTag(exactMatch: false)))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +70,7 @@ public extension Configuration {
|
|||||||
struct PreRelease: Codable, Equatable, Sendable {
|
struct PreRelease: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
public let prefix: String?
|
public let prefix: String?
|
||||||
|
// TODO: Remove optional.
|
||||||
public let strategy: Strategy?
|
public let strategy: Strategy?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
@@ -81,6 +84,7 @@ public extension Configuration {
|
|||||||
public enum Strategy: Codable, Equatable, Sendable {
|
public enum Strategy: Codable, Equatable, Sendable {
|
||||||
case branch(includeCommitSha: Bool = true)
|
case branch(includeCommitSha: Bool = true)
|
||||||
case command(arguments: [String])
|
case command(arguments: [String])
|
||||||
|
// TODO: Remove.
|
||||||
case gitTag
|
case gitTag
|
||||||
|
|
||||||
public var branch: Branch? {
|
public var branch: Branch? {
|
||||||
@@ -97,23 +101,36 @@ public extension Configuration {
|
|||||||
///
|
///
|
||||||
struct SemVar: Codable, Equatable, Sendable {
|
struct SemVar: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
|
public let allowPreRelease: Bool?
|
||||||
|
|
||||||
/// Optional pre-releas suffix strategy.
|
/// Optional pre-releas suffix strategy.
|
||||||
public let preRelease: PreRelease?
|
public let preRelease: PreRelease?
|
||||||
|
|
||||||
/// Fail if an existing version file does not exist in the target.
|
/// Fail if an existing version file does not exist in the target.
|
||||||
public let requireExistingFile: Bool
|
public let requireExistingFile: Bool?
|
||||||
|
|
||||||
/// Fail if an existing semvar is not parsed from the file or version generation strategy.
|
/// Fail if an existing semvar is not parsed from the file or version generation strategy.
|
||||||
public let requireExistingSemVar: Bool
|
public let requireExistingSemVar: Bool?
|
||||||
|
|
||||||
|
public let strategy: Strategy?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
|
allowPreRelease: Bool? = true,
|
||||||
preRelease: PreRelease? = nil,
|
preRelease: PreRelease? = nil,
|
||||||
requireExistingFile: Bool = true,
|
requireExistingFile: Bool? = true,
|
||||||
requireExistingSemVar: Bool = true
|
requireExistingSemVar: Bool? = true,
|
||||||
|
strategy: Strategy? = nil
|
||||||
) {
|
) {
|
||||||
|
self.allowPreRelease = allowPreRelease
|
||||||
self.preRelease = preRelease
|
self.preRelease = preRelease
|
||||||
self.requireExistingFile = requireExistingFile
|
self.requireExistingFile = requireExistingFile
|
||||||
self.requireExistingSemVar = requireExistingSemVar
|
self.requireExistingSemVar = requireExistingSemVar
|
||||||
|
self.strategy = strategy
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Strategy: Codable, Equatable, Sendable {
|
||||||
|
case command(arguments: [String])
|
||||||
|
case gitTag(exactMatch: Bool? = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -165,7 +182,7 @@ public extension Configuration {
|
|||||||
/// - fileName: The file name located in the module directory.
|
/// - fileName: The file name located in the module directory.
|
||||||
public init(
|
public init(
|
||||||
_ name: String,
|
_ name: String,
|
||||||
fileName: String? = "Version.swift"
|
fileName: String? = nil
|
||||||
) {
|
) {
|
||||||
self.name = name
|
self.name = name
|
||||||
self.fileName = fileName
|
self.fileName = fileName
|
||||||
@@ -218,9 +235,11 @@ public extension Configuration {
|
|||||||
case branch(includeCommitSha: Bool = true)
|
case branch(includeCommitSha: Bool = true)
|
||||||
|
|
||||||
case semvar(
|
case semvar(
|
||||||
|
allowPreRelease: Bool? = nil,
|
||||||
preRelease: PreRelease? = nil,
|
preRelease: PreRelease? = nil,
|
||||||
requireExistingFile: Bool? = nil,
|
requireExistingFile: Bool? = nil,
|
||||||
requireExistingSemVar: Bool? = nil
|
requireExistingSemVar: Bool? = nil,
|
||||||
|
strategy: SemVar.Strategy? = nil
|
||||||
)
|
)
|
||||||
|
|
||||||
public var branch: Branch? {
|
public var branch: Branch? {
|
||||||
@@ -231,12 +250,14 @@ public extension Configuration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public var semvar: SemVar? {
|
public var semvar: SemVar? {
|
||||||
guard case let .semvar(preRelease, requireExistingFile, requireExistingSemVar) = self
|
guard case let .semvar(allowPreRelease, preRelease, requireExistingFile, requireExistingSemVar, strategy) = self
|
||||||
else { return nil }
|
else { return nil }
|
||||||
return .init(
|
return .init(
|
||||||
|
allowPreRelease: allowPreRelease,
|
||||||
preRelease: preRelease,
|
preRelease: preRelease,
|
||||||
requireExistingFile: requireExistingFile ?? false,
|
requireExistingFile: requireExistingFile ?? false,
|
||||||
requireExistingSemVar: requireExistingSemVar ?? false
|
requireExistingSemVar: requireExistingSemVar ?? false,
|
||||||
|
strategy: strategy
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,9 +267,11 @@ public extension Configuration {
|
|||||||
|
|
||||||
public static func semvar(_ value: SemVar) -> Self {
|
public static func semvar(_ value: SemVar) -> Self {
|
||||||
.semvar(
|
.semvar(
|
||||||
|
allowPreRelease: value.allowPreRelease,
|
||||||
preRelease: value.preRelease,
|
preRelease: value.preRelease,
|
||||||
requireExistingFile: value.requireExistingFile,
|
requireExistingFile: value.requireExistingFile,
|
||||||
requireExistingSemVar: value.requireExistingSemVar
|
requireExistingSemVar: value.requireExistingSemVar,
|
||||||
|
strategy: value.strategy
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ public extension DependencyValues {
|
|||||||
@DependencyClient
|
@DependencyClient
|
||||||
public struct ConfigurationClient: Sendable {
|
public struct ConfigurationClient: Sendable {
|
||||||
|
|
||||||
|
/// The default file name for a configuration file.
|
||||||
|
public var defaultFileName: @Sendable () -> String = { "test.json" }
|
||||||
|
|
||||||
/// Find a configuration file in the given directory or in current working directory.
|
/// Find a configuration file in the given directory or in current working directory.
|
||||||
public var find: @Sendable (URL?) async throws -> URL?
|
public var find: @Sendable (URL?) async throws -> URL?
|
||||||
|
|
||||||
@@ -32,6 +35,25 @@ public struct ConfigurationClient: Sendable {
|
|||||||
}
|
}
|
||||||
return (try? await load(url)) ?? .default
|
return (try? await load(url)) ?? .default
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Loads configuration from the given path, or searches for the default file and loads it.
|
||||||
|
/// Optionally merges other configuration, then perform an operation with the loaded configuration.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - path: Optional file path of the configuration to load.
|
||||||
|
/// - other: Optional configuration to merge with the loaded configuration.
|
||||||
|
/// - operation: The operation to perform with the loaded configuration.
|
||||||
|
@discardableResult
|
||||||
|
public func withConfiguration<T>(
|
||||||
|
path: String?,
|
||||||
|
merging other: Configuration? = nil,
|
||||||
|
operation: (Configuration) async throws -> T
|
||||||
|
) async throws -> T {
|
||||||
|
let configuration = try await findAndLoad(
|
||||||
|
path != nil ? URL(filePath: path!) : nil
|
||||||
|
)
|
||||||
|
return try await operation(configuration.merging(other))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ConfigurationClient: DependencyKey {
|
extension ConfigurationClient: DependencyKey {
|
||||||
@@ -39,6 +61,7 @@ extension ConfigurationClient: DependencyKey {
|
|||||||
|
|
||||||
public static var liveValue: ConfigurationClient {
|
public static var liveValue: ConfigurationClient {
|
||||||
.init(
|
.init(
|
||||||
|
defaultFileName: { "\(Constants.defaultFileNameWithoutExtension).json" },
|
||||||
find: { try await findConfiguration($0) },
|
find: { try await findConfiguration($0) },
|
||||||
load: { try await loadConfiguration($0) },
|
load: { try await loadConfiguration($0) },
|
||||||
write: { try await writeConfiguration($0, to: $1) }
|
write: { try await writeConfiguration($0, to: $1) }
|
||||||
|
|||||||
156
Sources/LoggingExtensions/Formatter.swift
Normal file
156
Sources/LoggingExtensions/Formatter.swift
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import Dependencies
|
||||||
|
import Foundation
|
||||||
|
import Logging
|
||||||
|
import LoggingFormatAndPipe
|
||||||
|
import Rainbow
|
||||||
|
import ShellClient
|
||||||
|
|
||||||
|
// MARK: Custom colors.
|
||||||
|
|
||||||
|
extension String {
|
||||||
|
var orange: Self {
|
||||||
|
bit24(255, 165, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var magenta: Self {
|
||||||
|
bit24(238, 130, 238)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Logger.Level {
|
||||||
|
|
||||||
|
var coloredString: String {
|
||||||
|
switch self {
|
||||||
|
case .info:
|
||||||
|
return "\(self)".cyan
|
||||||
|
case .warning:
|
||||||
|
return "\(self)".orange.bold
|
||||||
|
case .debug:
|
||||||
|
return "\(self)".green
|
||||||
|
case .trace:
|
||||||
|
return "\(self)".yellow
|
||||||
|
case .error:
|
||||||
|
return "\(self)".red.bold
|
||||||
|
default:
|
||||||
|
return "\(self)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct LevelFormatter: LoggingFormatAndPipe.Formatter {
|
||||||
|
|
||||||
|
let basic: BasicFormatter
|
||||||
|
|
||||||
|
var timestampFormatter: DateFormatter { basic.timestampFormatter }
|
||||||
|
|
||||||
|
// swiftlint:disable function_parameter_count
|
||||||
|
func processLog(
|
||||||
|
level: Logger.Level,
|
||||||
|
message: Logger.Message,
|
||||||
|
prettyMetadata: String?,
|
||||||
|
file: String,
|
||||||
|
function: String,
|
||||||
|
line: UInt
|
||||||
|
) -> String {
|
||||||
|
let now = Date()
|
||||||
|
|
||||||
|
return basic.format.map { component -> String in
|
||||||
|
return processComponent(
|
||||||
|
component,
|
||||||
|
now: now,
|
||||||
|
level: level,
|
||||||
|
message: message,
|
||||||
|
prettyMetadata: prettyMetadata,
|
||||||
|
file: file,
|
||||||
|
function: function,
|
||||||
|
line: line
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.filter { $0.count > 0 }
|
||||||
|
.joined(separator: basic.separator ?? "")
|
||||||
|
}
|
||||||
|
|
||||||
|
public func processComponent(
|
||||||
|
_ component: LogComponent,
|
||||||
|
now: Date,
|
||||||
|
level: Logger.Level,
|
||||||
|
message: Logger.Message,
|
||||||
|
prettyMetadata: String?,
|
||||||
|
file: String,
|
||||||
|
function: String,
|
||||||
|
line: UInt
|
||||||
|
) -> String {
|
||||||
|
switch component {
|
||||||
|
case .level:
|
||||||
|
let maxLen = "\(Logger.Level.warning)".count
|
||||||
|
let paddingCount = (maxLen - "\(level)".count) / 2
|
||||||
|
var padding = ""
|
||||||
|
for _ in 0 ... paddingCount {
|
||||||
|
padding += " "
|
||||||
|
}
|
||||||
|
return "\(padding)\(level.coloredString)\(padding)"
|
||||||
|
case let .group(components):
|
||||||
|
return components.map { component -> String in
|
||||||
|
self.processComponent(
|
||||||
|
component,
|
||||||
|
now: now,
|
||||||
|
level: level,
|
||||||
|
message: message,
|
||||||
|
prettyMetadata: prettyMetadata,
|
||||||
|
file: file,
|
||||||
|
function: function,
|
||||||
|
line: line
|
||||||
|
)
|
||||||
|
}.joined()
|
||||||
|
case .message:
|
||||||
|
return basic.processComponent(
|
||||||
|
component,
|
||||||
|
now: now,
|
||||||
|
level: level,
|
||||||
|
message: message,
|
||||||
|
prettyMetadata: prettyMetadata,
|
||||||
|
file: file,
|
||||||
|
function: function,
|
||||||
|
line: line
|
||||||
|
).italic
|
||||||
|
default:
|
||||||
|
return basic.processComponent(
|
||||||
|
component,
|
||||||
|
now: now,
|
||||||
|
level: level,
|
||||||
|
message: message,
|
||||||
|
prettyMetadata: prettyMetadata,
|
||||||
|
file: file,
|
||||||
|
function: function,
|
||||||
|
line: line
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// swiftlint:enable function_parameter_count
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension LoggingOptions {
|
||||||
|
|
||||||
|
func makeLogger() -> Logger {
|
||||||
|
let formatters: [LogComponent] = [
|
||||||
|
.text(executableName.magenta),
|
||||||
|
.text(command.blue),
|
||||||
|
.level,
|
||||||
|
.group([
|
||||||
|
.text("-> "),
|
||||||
|
.message
|
||||||
|
])
|
||||||
|
]
|
||||||
|
return Logger(label: executableName) { _ in
|
||||||
|
LoggingFormatAndPipe.Handler(
|
||||||
|
formatter: LevelFormatter(basic: BasicFormatter(
|
||||||
|
formatters,
|
||||||
|
separator: " | "
|
||||||
|
)),
|
||||||
|
pipe: LoggerTextOutputStreamPipe.standardOutput
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
import Logging
|
import Logging
|
||||||
|
|
||||||
// TODO: Move.
|
|
||||||
@_spi(Internal)
|
@_spi(Internal)
|
||||||
public extension Logger.Level {
|
public extension Logger.Level {
|
||||||
|
|
||||||
init(verbose: Int) {
|
init(verbose: Int) {
|
||||||
switch verbose {
|
switch verbose {
|
||||||
case 1: self = .warning
|
case 1: self = .debug
|
||||||
case 2: self = .debug
|
case 2...: self = .trace
|
||||||
case 3...: self = .trace
|
|
||||||
default: self = .info
|
default: self = .info
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
28
Sources/LoggingExtensions/LoggingOptions.swift
Normal file
28
Sources/LoggingExtensions/LoggingOptions.swift
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import Dependencies
|
||||||
|
import ShellClient
|
||||||
|
|
||||||
|
public struct LoggingOptions: Equatable, Sendable {
|
||||||
|
|
||||||
|
let command: String
|
||||||
|
let executableName: String
|
||||||
|
let verbose: Int
|
||||||
|
|
||||||
|
public init(
|
||||||
|
executableName: String = "bump-version",
|
||||||
|
command: String,
|
||||||
|
verbose: Int
|
||||||
|
) {
|
||||||
|
self.executableName = executableName
|
||||||
|
self.command = command
|
||||||
|
self.verbose = verbose
|
||||||
|
}
|
||||||
|
|
||||||
|
public func withLogger<T>(_ operation: () async throws -> T) async rethrows -> T {
|
||||||
|
try await withDependencies {
|
||||||
|
$0.logger = makeLogger()
|
||||||
|
$0.logger.logLevel = .init(verbose: verbose)
|
||||||
|
} operation: {
|
||||||
|
try await operation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,14 +3,16 @@ import Foundation
|
|||||||
|
|
||||||
@main
|
@main
|
||||||
struct Application: AsyncParsableCommand {
|
struct Application: AsyncParsableCommand {
|
||||||
|
static let commandName = "bump-version"
|
||||||
|
|
||||||
static let configuration: CommandConfiguration = .init(
|
static let configuration: CommandConfiguration = .init(
|
||||||
commandName: "bump-version",
|
commandName: commandName,
|
||||||
version: VERSION ?? "0.0.0",
|
version: VERSION ?? "0.0.0",
|
||||||
subcommands: [
|
subcommands: [
|
||||||
BuildCommand.self,
|
BuildCommand.self,
|
||||||
BumpCommand.self,
|
BumpCommand.self,
|
||||||
GenerateCommand.self,
|
GenerateCommand.self,
|
||||||
UtilsCommand.self
|
ConfigCommand.self
|
||||||
],
|
],
|
||||||
defaultSubcommand: BumpCommand.self
|
defaultSubcommand: BumpCommand.self
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
import ArgumentParser
|
import ArgumentParser
|
||||||
import CliClient
|
import CliClient
|
||||||
|
import CliDoc
|
||||||
import Foundation
|
import Foundation
|
||||||
import ShellClient
|
import ShellClient
|
||||||
|
|
||||||
|
// NOTE: This command is only used with the build with version plugin.
|
||||||
struct BuildCommand: AsyncParsableCommand {
|
struct BuildCommand: AsyncParsableCommand {
|
||||||
|
static let commandName = "build"
|
||||||
|
|
||||||
static let configuration: CommandConfiguration = .init(
|
static let configuration: CommandConfiguration = .init(
|
||||||
commandName: "build",
|
commandName: Self.commandName,
|
||||||
abstract: "Used for the build with version plugin.",
|
abstract: Abstract.default("Used for the build with version plugin.").render(),
|
||||||
discussion: "This should generally not be interacted with directly, outside of the build plugin.",
|
discussion: Discussion {
|
||||||
|
"This should generally not be interacted with directly, outside of the build plugin."
|
||||||
|
},
|
||||||
shouldDisplay: false
|
shouldDisplay: false
|
||||||
)
|
)
|
||||||
|
|
||||||
@OptionGroup var globals: GlobalOptions
|
@OptionGroup var globals: GlobalOptions
|
||||||
|
|
||||||
func run() async throws {
|
func run() async throws {
|
||||||
try await globals.run(\.build)
|
try await globals.run(\.build, command: Self.commandName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,28 @@
|
|||||||
import ArgumentParser
|
import ArgumentParser
|
||||||
import CliClient
|
import CliClient
|
||||||
|
import CliDoc
|
||||||
import Dependencies
|
import Dependencies
|
||||||
|
|
||||||
struct BumpCommand: AsyncParsableCommand {
|
struct BumpCommand: CommandRepresentable {
|
||||||
|
|
||||||
|
static let commandName = "bump"
|
||||||
|
|
||||||
static let configuration = CommandConfiguration(
|
static let configuration = CommandConfiguration(
|
||||||
commandName: "bump",
|
commandName: Self.commandName,
|
||||||
abstract: "Bump version of a command-line tool."
|
abstract: Abstract.default("Bump version of a command-line tool."),
|
||||||
|
usage: Usage.default(commandName: nil),
|
||||||
|
discussion: Discussion.default(examples: [
|
||||||
|
makeExample(
|
||||||
|
label: "Basic usage, bump the minor version.",
|
||||||
|
example: "--minor",
|
||||||
|
includesAppName: false
|
||||||
|
),
|
||||||
|
makeExample(
|
||||||
|
label: "Dry run, just show what the bumped version would be.",
|
||||||
|
example: "--minor --dry-run",
|
||||||
|
includesAppName: false
|
||||||
|
)
|
||||||
|
])
|
||||||
)
|
)
|
||||||
|
|
||||||
@OptionGroup var globals: GlobalOptions
|
@OptionGroup var globals: GlobalOptions
|
||||||
@@ -19,7 +35,7 @@ struct BumpCommand: AsyncParsableCommand {
|
|||||||
var bumpOption: CliClient.BumpOption = .patch
|
var bumpOption: CliClient.BumpOption = .patch
|
||||||
|
|
||||||
func run() async throws {
|
func run() async throws {
|
||||||
try await globals.run(\.bump, args: bumpOption)
|
try await globals.run(\.bump, command: Self.commandName, args: bumpOption)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
232
Sources/bump-version/Commands/ConfigCommand.swift
Normal file
232
Sources/bump-version/Commands/ConfigCommand.swift
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
import ArgumentParser
|
||||||
|
import CliClient
|
||||||
|
import CliDoc
|
||||||
|
import ConfigurationClient
|
||||||
|
import CustomDump
|
||||||
|
import Dependencies
|
||||||
|
import FileClient
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct ConfigCommand: AsyncParsableCommand {
|
||||||
|
static let commandName = "config"
|
||||||
|
|
||||||
|
static let configuration = CommandConfiguration(
|
||||||
|
commandName: commandName,
|
||||||
|
abstract: Abstract.default("Configuration commands").render(),
|
||||||
|
subcommands: [
|
||||||
|
DumpConfig.self,
|
||||||
|
GenerateConfig.self
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ConfigCommand {
|
||||||
|
|
||||||
|
struct DumpConfig: CommandRepresentable {
|
||||||
|
static let commandName = "dump"
|
||||||
|
static let parentCommand = ConfigCommand.commandName
|
||||||
|
|
||||||
|
static let configuration = CommandConfiguration(
|
||||||
|
commandName: Self.commandName,
|
||||||
|
abstract: Abstract.default("Inspect the parsed configuration."),
|
||||||
|
usage: Usage.default(parentCommand: ConfigCommand.commandName, commandName: Self.commandName),
|
||||||
|
discussion: Discussion.default(
|
||||||
|
notes: [
|
||||||
|
"""
|
||||||
|
The default style is to print the output in `json`, however you can use the `--swift` flag to
|
||||||
|
print the output in `swift`.
|
||||||
|
"""
|
||||||
|
],
|
||||||
|
examples: [
|
||||||
|
makeExample(label: "Show the project configuration.", example: ""),
|
||||||
|
makeExample(
|
||||||
|
label: "Update a configuration file with the dumped output",
|
||||||
|
example: "--disable-pre-release > .bump-version.prod.json"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
) {
|
||||||
|
"""
|
||||||
|
Loads the project configuration file (if applicable) and merges the options passed in,
|
||||||
|
then prints the configuration to stdout.
|
||||||
|
"""
|
||||||
|
},
|
||||||
|
aliases: ["d"]
|
||||||
|
)
|
||||||
|
@Flag(
|
||||||
|
help: "Change the style of what get's printed."
|
||||||
|
)
|
||||||
|
fileprivate var printStyle: PrintStyle = .json
|
||||||
|
|
||||||
|
@OptionGroup var globals: ConfigCommandOptions
|
||||||
|
|
||||||
|
func run() async throws {
|
||||||
|
let configuration = try await globals
|
||||||
|
.shared(command: Self.commandName)
|
||||||
|
.runClient(\.parsedConfiguration)
|
||||||
|
|
||||||
|
try globals.printConfiguration(configuration, style: printStyle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GenerateConfig: CommandRepresentable {
|
||||||
|
static let commandName = "generate"
|
||||||
|
static let parentCommand = ConfigCommand.commandName
|
||||||
|
|
||||||
|
static let configuration: CommandConfiguration = .init(
|
||||||
|
commandName: commandName,
|
||||||
|
abstract: Abstract.default("Generate a configuration file, based on the given options.").render(),
|
||||||
|
usage: Usage.default(parentCommand: ConfigCommand.commandName, commandName: commandName),
|
||||||
|
discussion: Discussion.default(examples: [
|
||||||
|
makeExample(
|
||||||
|
label: "Generate a configuration file for the 'foo' target.",
|
||||||
|
example: "-m foo"
|
||||||
|
),
|
||||||
|
makeExample(
|
||||||
|
label: "Show the output and don't write to a file.",
|
||||||
|
example: "-m foo --print"
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
aliases: ["g"]
|
||||||
|
)
|
||||||
|
|
||||||
|
@Flag(
|
||||||
|
help: "The style of the configuration."
|
||||||
|
)
|
||||||
|
var style: ConfigCommand.Style = .semvar
|
||||||
|
|
||||||
|
@Flag(
|
||||||
|
name: .customLong("print"),
|
||||||
|
help: "Print json to stdout."
|
||||||
|
)
|
||||||
|
var printJson: Bool = false
|
||||||
|
|
||||||
|
@OptionGroup var globals: ConfigCommandOptions
|
||||||
|
|
||||||
|
func run() async throws {
|
||||||
|
try await withSetupDependencies {
|
||||||
|
@Dependency(\.configurationClient) var configurationClient
|
||||||
|
|
||||||
|
let configuration = try style.parseConfiguration(
|
||||||
|
configOptions: globals.configOptions,
|
||||||
|
extraOptions: globals.extraOptions
|
||||||
|
)
|
||||||
|
|
||||||
|
switch printJson {
|
||||||
|
case true:
|
||||||
|
try globals.handlePrintJson(configuration)
|
||||||
|
case false:
|
||||||
|
let url = globals.configFileUrl
|
||||||
|
try await configurationClient.write(configuration, url)
|
||||||
|
print(url.cleanFilePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ConfigCommand {
|
||||||
|
enum Style: EnumerableFlag {
|
||||||
|
case branch, semvar
|
||||||
|
|
||||||
|
func parseConfiguration(
|
||||||
|
configOptions: ConfigurationOptions,
|
||||||
|
extraOptions: [String]
|
||||||
|
) throws -> Configuration {
|
||||||
|
let strategy: Configuration.VersionStrategy
|
||||||
|
|
||||||
|
switch self {
|
||||||
|
case .branch:
|
||||||
|
strategy = .branch(includeCommitSha: configOptions.commitSha)
|
||||||
|
case .semvar:
|
||||||
|
strategy = try .semvar(configOptions.semvarOptions(extraOptions: extraOptions))
|
||||||
|
}
|
||||||
|
|
||||||
|
return try Configuration(
|
||||||
|
target: configOptions.target(),
|
||||||
|
strategy: strategy
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add verbose.
|
||||||
|
@dynamicMemberLookup
|
||||||
|
struct ConfigCommandOptions: ParsableArguments {
|
||||||
|
|
||||||
|
@OptionGroup var configOptions: ConfigurationOptions
|
||||||
|
|
||||||
|
@Flag(
|
||||||
|
name: .shortAndLong,
|
||||||
|
help: "Increase logging level, can be passed multiple times (example: -vvv)."
|
||||||
|
)
|
||||||
|
var verbose: Int
|
||||||
|
|
||||||
|
@Argument(
|
||||||
|
help: """
|
||||||
|
Arguments / options used for custom pre-release, options / flags must proceed a '--' in
|
||||||
|
the command. These are ignored if the `--custom-command` or `--custom-pre-release` flag is not set.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
var extraOptions: [String] = []
|
||||||
|
|
||||||
|
subscript<T>(dynamicMember keyPath: KeyPath<ConfigurationOptions, T>) -> T {
|
||||||
|
configOptions[keyPath: keyPath]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension ConfigCommand.DumpConfig {
|
||||||
|
enum PrintStyle: EnumerableFlag {
|
||||||
|
case json, swift
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension ConfigCommand.ConfigCommandOptions {
|
||||||
|
|
||||||
|
func shared(command: String) throws -> CliClient.SharedOptions {
|
||||||
|
try configOptions.shared(command: command, extraOptions: extraOptions, verbose: verbose)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePrintJson(_ configuration: Configuration) throws {
|
||||||
|
@Dependency(\.coders) var coders
|
||||||
|
@Dependency(\.logger) var logger
|
||||||
|
|
||||||
|
let data = try coders.jsonEncoder().encode(configuration)
|
||||||
|
guard let string = String(bytes: data, encoding: .utf8) else {
|
||||||
|
logger.error("Error encoding configuration to json.")
|
||||||
|
throw ConfigurationEncodingError()
|
||||||
|
}
|
||||||
|
print(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printConfiguration(
|
||||||
|
_ configuration: Configuration,
|
||||||
|
style: ConfigCommand.DumpConfig.PrintStyle
|
||||||
|
) throws {
|
||||||
|
switch style {
|
||||||
|
case .json:
|
||||||
|
try handlePrintJson(configuration)
|
||||||
|
case .swift:
|
||||||
|
customDump(configuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
// guard printJson else {
|
||||||
|
// customDump(configuration)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// try handlePrintJson(configuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension ConfigurationOptions {
|
||||||
|
var configFileUrl: URL {
|
||||||
|
switch configurationFile {
|
||||||
|
case let .some(path):
|
||||||
|
return URL(filePath: path)
|
||||||
|
case .none:
|
||||||
|
return URL(filePath: ".bump-version.json")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ConfigurationEncodingError: Error {}
|
||||||
@@ -1,19 +1,29 @@
|
|||||||
import ArgumentParser
|
import ArgumentParser
|
||||||
import CliClient
|
import CliClient
|
||||||
|
import CliDoc
|
||||||
import Dependencies
|
import Dependencies
|
||||||
import Foundation
|
import Foundation
|
||||||
import ShellClient
|
import ShellClient
|
||||||
|
|
||||||
struct GenerateCommand: AsyncParsableCommand {
|
struct GenerateCommand: CommandRepresentable {
|
||||||
|
static let commandName = "generate"
|
||||||
|
|
||||||
static let configuration: CommandConfiguration = .init(
|
static let configuration: CommandConfiguration = .init(
|
||||||
commandName: "generate",
|
commandName: Self.commandName,
|
||||||
abstract: "Generates a version file in a command line tool that can be set via the git tag or git sha.",
|
abstract: Abstract.default("Generates a version file in your project."),
|
||||||
discussion: "This command can be interacted with directly, outside of the plugin usage context."
|
usage: Usage.default(commandName: Self.commandName),
|
||||||
|
discussion: Discussion.default(
|
||||||
|
examples: [
|
||||||
|
makeExample(label: "Basic usage.", example: "")
|
||||||
|
]
|
||||||
|
) {
|
||||||
|
"This command can be interacted with directly, outside of the plugin usage context."
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@OptionGroup var globals: GlobalOptions
|
@OptionGroup var globals: GlobalOptions
|
||||||
|
|
||||||
func run() async throws {
|
func run() async throws {
|
||||||
try await globals.run(\.generate)
|
try await globals.run(\.generate, command: Self.commandName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
import ArgumentParser
|
|
||||||
import ConfigurationClient
|
|
||||||
import CustomDump
|
|
||||||
import Dependencies
|
|
||||||
import FileClient
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
struct UtilsCommand: AsyncParsableCommand {
|
|
||||||
static let configuration = CommandConfiguration(
|
|
||||||
commandName: "utils",
|
|
||||||
abstract: "Utility commands",
|
|
||||||
subcommands: [
|
|
||||||
DumpConfig.self,
|
|
||||||
GenerateConfig.self
|
|
||||||
]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
extension UtilsCommand {
|
|
||||||
struct DumpConfig: AsyncParsableCommand {
|
|
||||||
static let configuration = CommandConfiguration(
|
|
||||||
commandName: "dump-config",
|
|
||||||
abstract: "Show the parsed configuration.",
|
|
||||||
aliases: ["dc"]
|
|
||||||
)
|
|
||||||
|
|
||||||
@OptionGroup var globals: GlobalOptions
|
|
||||||
|
|
||||||
func run() async throws {
|
|
||||||
let configuration = try await globals.runClient(\.parsedConfiguration)
|
|
||||||
customDump(configuration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct GenerateConfig: AsyncParsableCommand {
|
|
||||||
static let configuration: CommandConfiguration = .init(
|
|
||||||
commandName: "generate-config",
|
|
||||||
abstract: "Generate a configuration file.",
|
|
||||||
aliases: ["gc"]
|
|
||||||
)
|
|
||||||
|
|
||||||
@OptionGroup var configOptions: ConfigurationOptions
|
|
||||||
|
|
||||||
@Flag(
|
|
||||||
help: "The style of the configuration."
|
|
||||||
)
|
|
||||||
var style: Style = .semvar
|
|
||||||
|
|
||||||
@Argument(
|
|
||||||
help: """
|
|
||||||
Arguments / options used for custom pre-release, options / flags must proceed a '--' in
|
|
||||||
the command. These are ignored if the `--custom` flag is not set.
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
var extraOptions: [String] = []
|
|
||||||
|
|
||||||
func run() async throws {
|
|
||||||
try await withSetupDependencies {
|
|
||||||
@Dependency(\.configurationClient) var configurationClient
|
|
||||||
|
|
||||||
let strategy: Configuration.VersionStrategy
|
|
||||||
|
|
||||||
switch style {
|
|
||||||
case .branch:
|
|
||||||
strategy = .branch(includeCommitSha: configOptions.commitSha)
|
|
||||||
case .semvar:
|
|
||||||
strategy = try .semvar(configOptions.semvarOptions(extraOptions: extraOptions))
|
|
||||||
}
|
|
||||||
|
|
||||||
let configuration = try Configuration(
|
|
||||||
target: configOptions.target(),
|
|
||||||
strategy: strategy
|
|
||||||
)
|
|
||||||
|
|
||||||
let url: URL
|
|
||||||
switch configOptions.configurationFile {
|
|
||||||
case let .some(path):
|
|
||||||
url = URL(filePath: path)
|
|
||||||
case .none:
|
|
||||||
url = URL(filePath: ".bump-version.json")
|
|
||||||
}
|
|
||||||
|
|
||||||
try await configurationClient.write(configuration, url)
|
|
||||||
|
|
||||||
print(url.cleanFilePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension UtilsCommand.GenerateConfig {
|
|
||||||
enum Style: EnumerableFlag {
|
|
||||||
case branch, semvar
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,8 @@ import Dependencies
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Rainbow
|
import Rainbow
|
||||||
|
|
||||||
|
// TODO: Add an option to not load project configuration.
|
||||||
|
|
||||||
struct GlobalOptions: ParsableArguments {
|
struct GlobalOptions: ParsableArguments {
|
||||||
|
|
||||||
@OptionGroup
|
@OptionGroup
|
||||||
@@ -31,7 +33,7 @@ struct GlobalOptions: ParsableArguments {
|
|||||||
@Argument(
|
@Argument(
|
||||||
help: """
|
help: """
|
||||||
Arguments / options used for custom pre-release, options / flags must proceed a '--' in
|
Arguments / options used for custom pre-release, options / flags must proceed a '--' in
|
||||||
the command. These are ignored if the `--custom` flag is not set.
|
the command. These are ignored if the `--custom` or `--custom-pre-release` flag is not set.
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
var extraOptions: [String] = []
|
var extraOptions: [String] = []
|
||||||
@@ -40,8 +42,8 @@ struct GlobalOptions: ParsableArguments {
|
|||||||
|
|
||||||
struct ConfigurationOptions: ParsableArguments {
|
struct ConfigurationOptions: ParsableArguments {
|
||||||
@Option(
|
@Option(
|
||||||
name: .shortAndLong,
|
name: [.customShort("f"), .long],
|
||||||
help: "Specify the path to a configuration file.",
|
help: "Specify the path to a configuration file. (default: .bump-version.json)",
|
||||||
completion: .file(extensions: ["json"])
|
completion: .file(extensions: ["json"])
|
||||||
)
|
)
|
||||||
var configurationFile: String?
|
var configurationFile: String?
|
||||||
@@ -63,22 +65,22 @@ struct ConfigurationOptions: ParsableArguments {
|
|||||||
|
|
||||||
struct TargetOptions: ParsableArguments {
|
struct TargetOptions: ParsableArguments {
|
||||||
@Option(
|
@Option(
|
||||||
name: .shortAndLong,
|
name: [.customShort("p"), .long],
|
||||||
help: "Path to the version file, not required if module is set."
|
help: "Path to the version file, not required if module is set."
|
||||||
)
|
)
|
||||||
var path: String?
|
var targetFilePath: String?
|
||||||
|
|
||||||
@Option(
|
@Option(
|
||||||
name: .shortAndLong,
|
name: [.customShort("m"), .long],
|
||||||
help: "The target module name or directory path, not required if path is set."
|
help: "The target module name or directory path, not required if path is set."
|
||||||
)
|
)
|
||||||
var module: String?
|
var targetModule: String?
|
||||||
|
|
||||||
@Option(
|
@Option(
|
||||||
name: [.customShort("n"), .long],
|
name: [.customShort("n"), .long],
|
||||||
help: "The file name inside the target module, required if module is set."
|
help: "The file name inside the target module. (defaults to: \"Version.swift\")."
|
||||||
)
|
)
|
||||||
var fileName: String = "Version.swift"
|
var targetFileName: String?
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +93,7 @@ struct PreReleaseOptions: ParsableArguments {
|
|||||||
var disablePreRelease: Bool = false
|
var disablePreRelease: Bool = false
|
||||||
|
|
||||||
@Flag(
|
@Flag(
|
||||||
name: [.customShort("s"), .customLong("pre-release-branch-style")],
|
name: [.customShort("b"), .customLong("pre-release-branch-style")],
|
||||||
help: """
|
help: """
|
||||||
Use branch name and commit sha for pre-release suffix, ignored if branch is set.
|
Use branch name and commit sha for pre-release suffix, ignored if branch is set.
|
||||||
"""
|
"""
|
||||||
@@ -124,8 +126,22 @@ struct PreReleaseOptions: ParsableArguments {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Add custom command strategy.
|
||||||
struct SemVarOptions: ParsableArguments {
|
struct SemVarOptions: ParsableArguments {
|
||||||
|
|
||||||
|
@Flag(
|
||||||
|
name: .long,
|
||||||
|
inversion: .prefixedEnableDisable,
|
||||||
|
help: "Use git-tag strategy for semvar."
|
||||||
|
)
|
||||||
|
var gitTag: Bool = true
|
||||||
|
|
||||||
|
@Flag(
|
||||||
|
name: .long,
|
||||||
|
help: "Require exact match for git tag strategy."
|
||||||
|
)
|
||||||
|
var requireExactMatch: Bool = false
|
||||||
|
|
||||||
@Flag(
|
@Flag(
|
||||||
name: .long,
|
name: .long,
|
||||||
help: """
|
help: """
|
||||||
@@ -136,9 +152,18 @@ struct SemVarOptions: ParsableArguments {
|
|||||||
|
|
||||||
@Flag(
|
@Flag(
|
||||||
name: .long,
|
name: .long,
|
||||||
help: "Fail if a sem-var is not parsed from existing file or git tag, used if branch is not set."
|
help: "Fail if a semvar is not parsed from existing file or git tag, used if branch is not set."
|
||||||
)
|
)
|
||||||
var requireExistingSemvar: Bool = false
|
var requireExistingSemvar: Bool = false
|
||||||
|
|
||||||
|
@Flag(
|
||||||
|
name: .shortAndLong,
|
||||||
|
help: """
|
||||||
|
Custom command strategy, uses extra-options to call an external command.
|
||||||
|
The external command should return a semvar that is used.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
var customCommand: Bool = false
|
||||||
|
|
||||||
@OptionGroup var preRelease: PreReleaseOptions
|
@OptionGroup var preRelease: PreReleaseOptions
|
||||||
}
|
}
|
||||||
|
|||||||
311
Sources/bump-version/Helpers/DocHelpers.swift
Normal file
311
Sources/bump-version/Helpers/DocHelpers.swift
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
/*
|
||||||
|
This file contains helpers for generating the documentation for the commands.
|
||||||
|
|
||||||
|
*/
|
||||||
|
import ArgumentParser
|
||||||
|
import CliDoc
|
||||||
|
import Rainbow
|
||||||
|
|
||||||
|
protocol CommandRepresentable: AsyncParsableCommand {
|
||||||
|
static var commandName: String { get }
|
||||||
|
static var parentCommand: String? { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CommandRepresentable {
|
||||||
|
|
||||||
|
static var parentCommand: String? { nil }
|
||||||
|
|
||||||
|
static func makeExample(
|
||||||
|
label: String,
|
||||||
|
example: String,
|
||||||
|
includesAppName: Bool = true
|
||||||
|
) -> AppExample {
|
||||||
|
.init(
|
||||||
|
label: label,
|
||||||
|
parentCommand: parentCommand,
|
||||||
|
commandName: commandName,
|
||||||
|
includesAppName: includesAppName,
|
||||||
|
example: example
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Abstract where Content == String {
|
||||||
|
static func `default`(_ string: String) -> Self {
|
||||||
|
.init { string.blue }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Note<Content: TextNode>: TextNode {
|
||||||
|
let content: Content
|
||||||
|
|
||||||
|
init(
|
||||||
|
@TextBuilder _ content: () -> Content
|
||||||
|
) {
|
||||||
|
self.content = content()
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some TextNode {
|
||||||
|
LabeledContent {
|
||||||
|
content.italic()
|
||||||
|
} label: {
|
||||||
|
"Note:".yellow.bold
|
||||||
|
}
|
||||||
|
.style(.vertical())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Note where Content == AnyTextNode {
|
||||||
|
|
||||||
|
static func `default`(
|
||||||
|
notes: [String],
|
||||||
|
usesConfigurationFileNote: Bool = true,
|
||||||
|
usesConfigurationMergingNote: Bool = true
|
||||||
|
) -> Self {
|
||||||
|
var notes = notes
|
||||||
|
|
||||||
|
if usesConfigurationFileNote {
|
||||||
|
notes.insert(
|
||||||
|
"Most options are not required when a configuration file is setup for your project.",
|
||||||
|
at: 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if usesConfigurationMergingNote {
|
||||||
|
if usesConfigurationFileNote {
|
||||||
|
notes.insert(
|
||||||
|
"Any configuration options get merged with the loaded project configuration file.",
|
||||||
|
at: 1
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
notes.insert(
|
||||||
|
"Any configuration options get merged with the loaded project configuration file.",
|
||||||
|
at: 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return .init {
|
||||||
|
VStack {
|
||||||
|
notes.enumerated().map { "\($0 + 1). \($1)" }
|
||||||
|
}
|
||||||
|
.eraseToAnyTextNode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Discussion where Content == AnyTextNode {
|
||||||
|
static func `default`<Preamble: TextNode, Trailing: TextNode>(
|
||||||
|
notes: [String] = [],
|
||||||
|
examples: [AppExample]? = nil,
|
||||||
|
usesExtraOptions: Bool = true,
|
||||||
|
usesConfigurationFileNote: Bool = true,
|
||||||
|
usesConfigurationMergingNote: Bool = true,
|
||||||
|
@TextBuilder preamble: () -> Preamble,
|
||||||
|
@TextBuilder trailing: () -> Trailing
|
||||||
|
) -> Self {
|
||||||
|
Discussion {
|
||||||
|
VStack {
|
||||||
|
preamble().italic()
|
||||||
|
|
||||||
|
Note.default(
|
||||||
|
notes: notes,
|
||||||
|
usesConfigurationFileNote: usesConfigurationFileNote,
|
||||||
|
usesConfigurationMergingNote: usesConfigurationMergingNote
|
||||||
|
)
|
||||||
|
|
||||||
|
if let examples {
|
||||||
|
ExampleSection.default(examples: examples, usesExtraOptions: usesExtraOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
trailing()
|
||||||
|
}
|
||||||
|
.separator(.newLine(count: 2))
|
||||||
|
.eraseToAnyTextNode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func `default`<Preamble: TextNode>(
|
||||||
|
notes: [String] = [],
|
||||||
|
examples: [AppExample]? = nil,
|
||||||
|
usesExtraOptions: Bool = true,
|
||||||
|
usesConfigurationFileNote: Bool = true,
|
||||||
|
usesConfigurationMergingNote: Bool = true,
|
||||||
|
@TextBuilder preamble: () -> Preamble
|
||||||
|
) -> Self {
|
||||||
|
.default(
|
||||||
|
notes: notes,
|
||||||
|
examples: examples,
|
||||||
|
usesExtraOptions: usesExtraOptions,
|
||||||
|
usesConfigurationFileNote: usesConfigurationFileNote,
|
||||||
|
usesConfigurationMergingNote: usesConfigurationMergingNote,
|
||||||
|
preamble: preamble,
|
||||||
|
trailing: {
|
||||||
|
if usesExtraOptions {
|
||||||
|
ImportantNote.extraOptionsNote
|
||||||
|
} else {
|
||||||
|
Empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func `default`(
|
||||||
|
notes: [String] = [],
|
||||||
|
examples: [AppExample]? = nil,
|
||||||
|
usesExtraOptions: Bool = true,
|
||||||
|
usesConfigurationFileNote: Bool = true,
|
||||||
|
usesConfigurationMergingNote: Bool = true
|
||||||
|
) -> Self {
|
||||||
|
.default(
|
||||||
|
notes: notes,
|
||||||
|
examples: examples,
|
||||||
|
usesExtraOptions: usesExtraOptions,
|
||||||
|
usesConfigurationFileNote: usesConfigurationFileNote,
|
||||||
|
usesConfigurationMergingNote: usesConfigurationMergingNote,
|
||||||
|
preamble: { Empty() },
|
||||||
|
trailing: { Empty() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ExampleSection where Header == String, Label == String {
|
||||||
|
static func `default`(
|
||||||
|
examples: [AppExample] = [],
|
||||||
|
usesExtraOptions: Bool = true
|
||||||
|
) -> some TextNode {
|
||||||
|
var examples: [AppExample] = examples
|
||||||
|
if usesExtraOptions {
|
||||||
|
examples = examples.appendingExtraOptionsExample()
|
||||||
|
}
|
||||||
|
|
||||||
|
return Self(
|
||||||
|
"Examples:",
|
||||||
|
label: "A few common usage examples.",
|
||||||
|
examples: examples.map(\.example)
|
||||||
|
)
|
||||||
|
.style(AppExampleSectionStyle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AppExampleSectionStyle: ExampleSectionStyle {
|
||||||
|
|
||||||
|
func render(content: ExampleSectionConfiguration) -> some TextNode {
|
||||||
|
Section {
|
||||||
|
VStack {
|
||||||
|
content.examples.map { example in
|
||||||
|
VStack {
|
||||||
|
example.label.color(.green).bold()
|
||||||
|
ShellCommand(example.example).style(.default)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.separator(.newLine(count: 2))
|
||||||
|
} header: {
|
||||||
|
HStack {
|
||||||
|
content.title.color(.blue).bold()
|
||||||
|
content.label.italic()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AppExample {
|
||||||
|
let label: String
|
||||||
|
let parentCommand: String?
|
||||||
|
let commandName: String
|
||||||
|
let includesAppName: Bool
|
||||||
|
let exampleText: String
|
||||||
|
|
||||||
|
init(
|
||||||
|
label: String,
|
||||||
|
parentCommand: String? = nil,
|
||||||
|
commandName: String,
|
||||||
|
includesAppName: Bool = true,
|
||||||
|
example exampleText: String
|
||||||
|
) {
|
||||||
|
self.label = label
|
||||||
|
self.parentCommand = parentCommand
|
||||||
|
self.commandName = commandName
|
||||||
|
self.includesAppName = includesAppName
|
||||||
|
self.exampleText = exampleText
|
||||||
|
}
|
||||||
|
|
||||||
|
var example: Example {
|
||||||
|
var exampleString = "\(commandName) \(exampleText)"
|
||||||
|
if let parentCommand {
|
||||||
|
exampleString = "\(parentCommand) \(exampleString)"
|
||||||
|
}
|
||||||
|
if includesAppName {
|
||||||
|
exampleString = "\(Application.commandName) \(exampleString)"
|
||||||
|
}
|
||||||
|
return (label: label, example: exampleString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Array where Element == AppExample {
|
||||||
|
|
||||||
|
func appendingExtraOptionsExample() -> Self {
|
||||||
|
guard let first = first else { return self }
|
||||||
|
var output = self
|
||||||
|
output.append(.init(
|
||||||
|
label: "Passing extra options to custom strategy.",
|
||||||
|
parentCommand: first.parentCommand,
|
||||||
|
commandName: first.commandName,
|
||||||
|
includesAppName: first.includesAppName,
|
||||||
|
example: "--custom-command -- git describe --tags --exact-match"
|
||||||
|
))
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ImportantNote<Content: TextNode>: TextNode {
|
||||||
|
let content: Content
|
||||||
|
|
||||||
|
init(
|
||||||
|
@TextBuilder _ content: () -> Content
|
||||||
|
) {
|
||||||
|
self.content = content()
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some TextNode {
|
||||||
|
LabeledContent {
|
||||||
|
content.italic()
|
||||||
|
} label: {
|
||||||
|
"Important Note:".red.bold
|
||||||
|
}
|
||||||
|
.style(.vertical())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ImportantNote where Content == String {
|
||||||
|
static var extraOptionsNote: Self {
|
||||||
|
.init {
|
||||||
|
"""
|
||||||
|
Extra options / flags for custom strategies must proceed a `--` or you may get an undefined option error.
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Usage where Content == AnyTextNode {
|
||||||
|
static func `default`(parentCommand: String? = nil, commandName: String?) -> Self {
|
||||||
|
var commandString = commandName == nil ? "" : "\(commandName!)"
|
||||||
|
if let parentCommand {
|
||||||
|
commandString = "\(parentCommand) \(commandString)"
|
||||||
|
}
|
||||||
|
commandString = commandString == "" ? "\(Application.commandName)" : "\(Application.commandName) \(commandString)"
|
||||||
|
|
||||||
|
return .init {
|
||||||
|
HStack {
|
||||||
|
commandString.blue
|
||||||
|
"[<options>]".green
|
||||||
|
"--"
|
||||||
|
"[<extra-options> ...]".cyan
|
||||||
|
}
|
||||||
|
.eraseToAnyTextNode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,72 +18,76 @@ func withSetupDependencies<T>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension GlobalOptions {
|
extension CliClient.SharedOptions {
|
||||||
|
|
||||||
func runClient<T>(
|
func runClient<T>(
|
||||||
_ keyPath: KeyPath<CliClient, @Sendable (CliClient.SharedOptions) async throws -> T>
|
_ keyPath: KeyPath<CliClient, @Sendable (Self) async throws -> T>
|
||||||
) async throws -> T {
|
) async throws -> T {
|
||||||
try await withSetupDependencies {
|
try await withSetupDependencies {
|
||||||
@Dependency(\.cliClient) var cliClient
|
@Dependency(\.cliClient) var cliClient
|
||||||
return try await cliClient[keyPath: keyPath](shared())
|
return try await cliClient[keyPath: keyPath](self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func runClient<A, T>(
|
func runClient<A, T>(
|
||||||
_ keyPath: KeyPath<CliClient, @Sendable (A, CliClient.SharedOptions) async throws -> T>,
|
_ keyPath: KeyPath<CliClient, @Sendable (A, Self) async throws -> T>,
|
||||||
args: A
|
args: A
|
||||||
) async throws -> T {
|
) async throws -> T {
|
||||||
try await withSetupDependencies {
|
try await withSetupDependencies {
|
||||||
@Dependency(\.cliClient) var cliClient
|
@Dependency(\.cliClient) var cliClient
|
||||||
return try await cliClient[keyPath: keyPath](args, shared())
|
return try await cliClient[keyPath: keyPath](args, self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension GlobalOptions {
|
||||||
|
|
||||||
func run(
|
func run(
|
||||||
_ keyPath: KeyPath<CliClient, @Sendable (CliClient.SharedOptions) async throws -> String>
|
_ keyPath: KeyPath<CliClient, @Sendable (CliClient.SharedOptions) async throws -> String>,
|
||||||
|
command: String
|
||||||
) async throws {
|
) async throws {
|
||||||
let output = try await runClient(keyPath)
|
let output = try await shared(command: command).runClient(keyPath)
|
||||||
print(output)
|
print(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
func run<T>(
|
func run<T>(
|
||||||
_ keyPath: KeyPath<CliClient, @Sendable (T, CliClient.SharedOptions) async throws -> String>,
|
_ keyPath: KeyPath<CliClient, @Sendable (T, CliClient.SharedOptions) async throws -> String>,
|
||||||
|
command: String,
|
||||||
args: T
|
args: T
|
||||||
) async throws {
|
) async throws {
|
||||||
let output = try await runClient(keyPath, args: args)
|
let output = try await shared(command: command).runClient(keyPath, args: args)
|
||||||
print(output)
|
print(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
func shared() throws -> CliClient.SharedOptions {
|
func shared(command: String) throws -> CliClient.SharedOptions {
|
||||||
try .init(
|
try configOptions.shared(
|
||||||
allowPreReleaseTag: !configOptions.semvarOptions.preRelease.disablePreRelease,
|
command: command,
|
||||||
dryRun: dryRun,
|
dryRun: dryRun,
|
||||||
|
extraOptions: extraOptions,
|
||||||
gitDirectory: gitDirectory,
|
gitDirectory: gitDirectory,
|
||||||
verbose: verbose,
|
verbose: verbose
|
||||||
target: configOptions.target(),
|
|
||||||
branch: .init(includeCommitSha: configOptions.commitSha),
|
|
||||||
semvar: configOptions.semvarOptions(extraOptions: extraOptions),
|
|
||||||
configurationFile: configOptions.configurationFile
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension TargetOptions {
|
private extension TargetOptions {
|
||||||
func configTarget() throws -> Configuration.Target? {
|
func configTarget() throws -> Configuration.Target? {
|
||||||
guard let path else {
|
guard let targetFilePath else {
|
||||||
guard let module else {
|
guard let targetModule else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return .init(module: .init(module, fileName: fileName))
|
return .init(module: .init(targetModule, fileName: targetFileName))
|
||||||
}
|
}
|
||||||
return .init(path: path)
|
return .init(path: targetFilePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension PreReleaseOptions {
|
extension PreReleaseOptions {
|
||||||
|
|
||||||
func configPreReleaseStrategy(includeCommitSha: Bool, extraOptions: [String]) throws -> Configuration.PreRelease? {
|
func configPreReleaseStrategy(
|
||||||
|
includeCommitSha: Bool,
|
||||||
|
extraOptions: [String]
|
||||||
|
) throws -> Configuration.PreRelease? {
|
||||||
if useBranchAsPreRelease {
|
if useBranchAsPreRelease {
|
||||||
return .init(prefix: preReleasePrefix, strategy: .branch(includeCommitSha: includeCommitSha))
|
return .init(prefix: preReleasePrefix, strategy: .branch(includeCommitSha: includeCommitSha))
|
||||||
} else if useTagAsPreRelease {
|
} else if useTagAsPreRelease {
|
||||||
@@ -93,6 +97,8 @@ extension PreReleaseOptions {
|
|||||||
throw ExtraOptionsEmpty()
|
throw ExtraOptionsEmpty()
|
||||||
}
|
}
|
||||||
return .init(prefix: preReleasePrefix, strategy: .command(arguments: extraOptions))
|
return .init(prefix: preReleasePrefix, strategy: .command(arguments: extraOptions))
|
||||||
|
} else if let preReleasePrefix {
|
||||||
|
return .init(prefix: preReleasePrefix, strategy: nil)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -100,11 +106,48 @@ extension PreReleaseOptions {
|
|||||||
|
|
||||||
extension SemVarOptions {
|
extension SemVarOptions {
|
||||||
|
|
||||||
func configSemVarOptions(includeCommitSha: Bool, extraOptions: [String]) throws -> Configuration.SemVar {
|
func parseStrategy(extraOptions: [String]) throws -> Configuration.SemVar.Strategy? {
|
||||||
try .init(
|
@Dependency(\.logger) var logger
|
||||||
preRelease: preRelease.configPreReleaseStrategy(includeCommitSha: includeCommitSha, extraOptions: extraOptions),
|
|
||||||
requireExistingFile: requireExistingFile,
|
guard customCommand else {
|
||||||
requireExistingSemVar: requireExistingSemvar
|
guard gitTag else { return nil }
|
||||||
|
return .gitTag(exactMatch: requireExactMatch)
|
||||||
|
}
|
||||||
|
guard extraOptions.count > 0 else {
|
||||||
|
logger.error("""
|
||||||
|
Extra options are empty, this does not make sense when using a custom command
|
||||||
|
strategy.
|
||||||
|
""")
|
||||||
|
throw ExtraOptionsEmpty()
|
||||||
|
}
|
||||||
|
return .command(arguments: extraOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func configSemVarOptions(
|
||||||
|
includeCommitSha: Bool,
|
||||||
|
extraOptions: [String]
|
||||||
|
) throws -> Configuration.SemVar {
|
||||||
|
@Dependency(\.logger) var logger
|
||||||
|
|
||||||
|
// TODO: Update when / if there's an update config command.
|
||||||
|
if customCommand && preRelease.customPreRelease {
|
||||||
|
logger.warning("""
|
||||||
|
Custom pre-release can not be used at same time as custom command.
|
||||||
|
Ignoring pre-release...
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
return try .init(
|
||||||
|
allowPreRelease: !preRelease.disablePreRelease,
|
||||||
|
preRelease: customCommand ? nil : preRelease.configPreReleaseStrategy(
|
||||||
|
includeCommitSha: includeCommitSha,
|
||||||
|
extraOptions: extraOptions
|
||||||
|
),
|
||||||
|
// Use nil here if false, which makes them not get used in json / file output, which makes
|
||||||
|
// user config smaller.
|
||||||
|
requireExistingFile: requireExistingFile ? true : nil,
|
||||||
|
requireExistingSemVar: requireExistingSemvar ? true : nil,
|
||||||
|
strategy: parseStrategy(extraOptions: extraOptions)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,8 +158,32 @@ extension ConfigurationOptions {
|
|||||||
try targetOptions.configTarget()
|
try targetOptions.configTarget()
|
||||||
}
|
}
|
||||||
|
|
||||||
func semvarOptions(extraOptions: [String]) throws -> Configuration.SemVar {
|
func semvarOptions(
|
||||||
try semvarOptions.configSemVarOptions(includeCommitSha: commitSha, extraOptions: extraOptions)
|
extraOptions: [String]
|
||||||
|
) throws -> Configuration.SemVar {
|
||||||
|
try semvarOptions.configSemVarOptions(
|
||||||
|
includeCommitSha: commitSha,
|
||||||
|
extraOptions: extraOptions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func shared(
|
||||||
|
command: String,
|
||||||
|
dryRun: Bool = true,
|
||||||
|
extraOptions: [String] = [],
|
||||||
|
gitDirectory: String? = nil,
|
||||||
|
verbose: Int = 0
|
||||||
|
) throws -> CliClient.SharedOptions {
|
||||||
|
try .init(
|
||||||
|
allowPreReleaseTag: !semvarOptions.preRelease.disablePreRelease,
|
||||||
|
dryRun: dryRun,
|
||||||
|
gitDirectory: gitDirectory,
|
||||||
|
loggingOptions: .init(command: command, verbose: verbose),
|
||||||
|
target: target(),
|
||||||
|
branch: semvarOptions.gitTag ? nil : .init(includeCommitSha: commitSha),
|
||||||
|
semvar: semvarOptions(extraOptions: extraOptions),
|
||||||
|
configurationFile: configurationFile
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ struct CliClientTests {
|
|||||||
#expect(output == "/baz/Sources/bar/Version.swift")
|
#expect(output == "/baz/Sources/bar/Version.swift")
|
||||||
} assert: { string, _ in
|
} assert: { string, _ in
|
||||||
|
|
||||||
|
if type != .preRelease {
|
||||||
#expect(string != nil)
|
#expect(string != nil)
|
||||||
|
}
|
||||||
let typeString = optional ? "String?" : "String"
|
let typeString = optional ? "String?" : "String"
|
||||||
|
|
||||||
switch type {
|
switch type {
|
||||||
@@ -132,13 +134,12 @@ extension CliClient.SharedOptions {
|
|||||||
gitDirectory: String? = "/baz",
|
gitDirectory: String? = "/baz",
|
||||||
dryRun: Bool = false,
|
dryRun: Bool = false,
|
||||||
target: String = "bar",
|
target: String = "bar",
|
||||||
logLevel: Logger.Level = .trace,
|
|
||||||
versionStrategy: Configuration.VersionStrategy = .semvar(.init())
|
versionStrategy: Configuration.VersionStrategy = .semvar(.init())
|
||||||
) -> Self {
|
) -> Self {
|
||||||
return .init(
|
return .init(
|
||||||
dryRun: dryRun,
|
dryRun: dryRun,
|
||||||
gitDirectory: gitDirectory,
|
gitDirectory: gitDirectory,
|
||||||
logLevel: logLevel,
|
loggingOptions: .init(command: "test", verbose: 2),
|
||||||
target: .init(module: .init(target)),
|
target: .init(module: .init(target)),
|
||||||
branch: versionStrategy.branch,
|
branch: versionStrategy.branch,
|
||||||
semvar: versionStrategy.semvar
|
semvar: versionStrategy.semvar
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import ConfigurationClient
|
@_spi(Internal) import ConfigurationClient
|
||||||
import Dependencies
|
import Dependencies
|
||||||
import Foundation
|
import Foundation
|
||||||
import Testing
|
import Testing
|
||||||
@@ -74,6 +74,64 @@ struct ConfigurationClientTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func mergingBranch() {
|
||||||
|
let branch = Configuration.Branch(includeCommitSha: false)
|
||||||
|
let branch2 = Configuration.Branch(includeCommitSha: true)
|
||||||
|
let merged = branch.merging(branch2)
|
||||||
|
#expect(merged == branch2)
|
||||||
|
|
||||||
|
let merged2 = branch.merging(nil)
|
||||||
|
#expect(merged2 == branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func mergingSemvar() {
|
||||||
|
let strategy1 = Configuration.VersionStrategy.semvar(.init())
|
||||||
|
let other = Configuration.VersionStrategy.semvar(.init(
|
||||||
|
allowPreRelease: true,
|
||||||
|
preRelease: .init(prefix: "foo", strategy: .gitTag),
|
||||||
|
requireExistingFile: true,
|
||||||
|
requireExistingSemVar: true,
|
||||||
|
strategy: .gitTag()
|
||||||
|
))
|
||||||
|
let merged = strategy1.merging(other)
|
||||||
|
#expect(merged == other)
|
||||||
|
|
||||||
|
let otherMerged = other.merging(strategy1)
|
||||||
|
#expect(otherMerged == other)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func mergingTarget() {
|
||||||
|
let config1 = Configuration(target: .init(path: "foo"))
|
||||||
|
let config2 = Configuration(target: .init(module: .init("bar")))
|
||||||
|
|
||||||
|
let merged = config1.merging(config2)
|
||||||
|
#expect(merged.target! == .init(module: .init("bar")))
|
||||||
|
|
||||||
|
let merged2 = merged.merging(config1)
|
||||||
|
#expect(merged2.target! == .init(path: "foo"))
|
||||||
|
|
||||||
|
let merged3 = merged2.merging(nil)
|
||||||
|
#expect(merged3 == merged2)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func mergingVersionStrategy() {
|
||||||
|
let version = Configuration.VersionStrategy.semvar(.init())
|
||||||
|
let version2 = Configuration.VersionStrategy.branch(.init())
|
||||||
|
|
||||||
|
let merged = version.merging(version2)
|
||||||
|
#expect(merged == version2)
|
||||||
|
|
||||||
|
let merged2 = merged.merging(.branch(includeCommitSha: false))
|
||||||
|
#expect(merged2.branch!.includeCommitSha == false)
|
||||||
|
|
||||||
|
let merged3 = version2.merging(version)
|
||||||
|
#expect(merged3 == version)
|
||||||
|
}
|
||||||
|
|
||||||
func run(
|
func run(
|
||||||
setupDependencies: @escaping (inout DependencyValues) -> Void = { _ in },
|
setupDependencies: @escaping (inout DependencyValues) -> Void = { _ in },
|
||||||
operation: () async throws -> Void
|
operation: () async throws -> Void
|
||||||
|
|||||||
Reference in New Issue
Block a user