feat: Renames some options, adds a require-configuration option that fails if configuration is not found.
All checks were successful
CI / Ubuntu (push) Successful in 2m19s

This commit is contained in:
2024-12-27 16:42:48 -05:00
parent 4420bd428a
commit 20f430fb8f
16 changed files with 133 additions and 154 deletions

View File

@@ -3,13 +3,15 @@
"semvar" : { "semvar" : {
"allowPreRelease" : true, "allowPreRelease" : true,
"strategy" : { "strategy" : {
"command" : { "gitTag" : {
"arguments" : [ "exactMatch" : false
"my-command",
"--some-option"
]
} }
} }
} }
},
"target" : {
"module" : {
"name" : "BumpVersion"
}
} }
} }

View File

@@ -18,7 +18,7 @@ struct BumpCommand: CommandRepresentable {
), ),
makeExample( makeExample(
label: "Dry run, just show what the bumped version would be.", label: "Dry run, just show what the bumped version would be.",
example: "--minor --dry-run" example: "--minor --print"
) )
]) ])
) )

View File

@@ -149,8 +149,6 @@ extension ConfigCommand {
} }
} }
// TODO: Add verbose.
// TODO: Need to be able to generate a branch style config file.
@dynamicMemberLookup @dynamicMemberLookup
struct ConfigCommandOptions: ParsableArguments { struct ConfigCommandOptions: ParsableArguments {
@@ -210,12 +208,6 @@ private extension ConfigCommand.ConfigCommandOptions {
case .swift: case .swift:
customDump(configuration) customDump(configuration)
} }
// guard printJson else {
// customDump(configuration)
// return
// }
// try handlePrintJson(configuration)
} }
} }

View File

@@ -10,7 +10,9 @@ by the module that a `Version.swift` file resides in. It also declares the strat
new versions. new versions.
The command-line tool comes with a command to generate the configuration file for you, this should The command-line tool comes with a command to generate the configuration file for you, this should
be ran from the root of your project. be ran from the root of your project or by specifying the path to write the configuration file to
using the `-f | --configuration-file` option. The below examples assume that you're running in the
root project directory.
```bash ```bash
bump-version config generate --target-module my-tool bump-version config generate --target-module my-tool

View File

@@ -39,10 +39,18 @@ are ignored if your configuration or options specify to use a `branch` strategy.
bump-version bump --minor bump-version bump --minor
``` ```
If you want to use the default configuration without generating your own project configuration, then
you can specify the path or module to the bump command. The default configuration will use the
`gitTag` strategy without any pre-release strategy.
```bash
bump-version bump --minor --target-module my-tool
```
Show the output, but don't update the version file. Show the output, but don't update the version file.
```bash ```bash
bump-version bump --major --dry-run bump-version bump --major --print
``` ```
### Generate Command ### Generate Command
@@ -66,11 +74,10 @@ Generates a configuration file based on the passed in options.
The following options are used to declare strategy used for deriving the version. The following options are used to declare strategy used for deriving the version.
| Long | Description | | Long | Description |
| -------- | ------------------------------------------------------- | | -------- | --------------------------------- |
| --branch | Use the branch strategy | | --branch | Use the branch strategy |
| --semvar | Use the semvar strategy (default) | | --semvar | Use the semvar strategy (default) |
| --print | Print the output to stdout instead of generating a file |
##### Generate Configuration Example ##### Generate Configuration Example

View File

@@ -9,13 +9,13 @@ of their usage.
### General Options ### General Options
| Short | Long | Argument | Description | | Short | Long | Argument | Description |
| ----- | --------------- | -------- | -------------------------------------------------------------------- | | ----- | ------------------- | -------- | -------------------------------------------------------------------- |
| N/A | --dry-run | N/A | Perform the command, but don't write any output files | | N/A | --print | N/A | Perform the command, but don't write any output files |
| N/A | --git-directory | <path> | The path to the root of your project, defaults to current directory | | N/A | --project-directory | <path> | The path to the root of your project, defaults to current directory |
| -h | --help | N/A | Show help for a command | | -h | --help | N/A | Show help for a command |
| -v | --verbose | N/A | Increase logging level, can be passed multiple times (example: -vvv) | | -v | --verbose | N/A | Increase logging level, can be passed multiple times (example: -vvv) |
| N/A | --version | N/A | Show the version of the command line tool | | N/A | --version | N/A | Show the version of the command line tool |
### Configuration Options ### Configuration Options
@@ -30,6 +30,7 @@ of their usage.
| N/A | --require-existing-semvar | N/A | Fail if an existing semvar is not found in the version file. | | N/A | --require-existing-semvar | N/A | Fail if an existing semvar is not found in the version file. |
| -c | --custom-command | <arguments> | Use a custom command strategy for the version (any options need to proceed a '--') | | -c | --custom-command | <arguments> | Use a custom command strategy for the version (any options need to proceed a '--') |
| N/A | --commit-sha/--no-commit-sha | N/A | Use the commit sha with branch version or pre-release strategy | | N/A | --commit-sha/--no-commit-sha | N/A | Use the commit sha with branch version or pre-release strategy |
| N/A | --require-configuration | N/A | Fail if a configuration file is not found |
#### Pre-Release Options #### Pre-Release Options

View File

@@ -13,13 +13,13 @@ struct GlobalOptions: ParsableArguments {
var configOptions: ConfigurationOptions var configOptions: ConfigurationOptions
@Option( @Option(
name: .customLong("git-directory"), name: .customLong("project-directory"),
help: "The git directory for the version (default: current directory)" help: "The project directory. (default: current directory)"
) )
var gitDirectory: String? var projectDirectory: String?
@Flag( @Flag(
name: .customLong("dry-run"), name: .customLong("print"),
help: "Print's what would be written to a target version file." help: "Print's what would be written to a target version file."
) )
var dryRun: Bool = false var dryRun: Bool = false
@@ -41,6 +41,7 @@ struct GlobalOptions: ParsableArguments {
} }
struct ConfigurationOptions: ParsableArguments { struct ConfigurationOptions: ParsableArguments {
@Option( @Option(
name: [.customShort("f"), .long], name: [.customShort("f"), .long],
help: "Specify the path to a configuration file. (default: .bump-version.json)", help: "Specify the path to a configuration file. (default: .bump-version.json)",
@@ -61,6 +62,14 @@ struct ConfigurationOptions: ParsableArguments {
) )
var commitSha: Bool = true var commitSha: Bool = true
@Flag(
name: .long,
help: """
Require a configuration file, otherwise fail.
"""
)
var requireConfiguration: Bool = false
} }
struct TargetOptions: ParsableArguments { struct TargetOptions: ParsableArguments {
@@ -126,7 +135,6 @@ struct PreReleaseOptions: ParsableArguments {
} }
// TODO: Add custom command strategy.
struct SemVarOptions: ParsableArguments { struct SemVarOptions: ParsableArguments {
@Flag( @Flag(

View File

@@ -64,7 +64,7 @@ extension GlobalOptions {
command: command, command: command,
dryRun: dryRun, dryRun: dryRun,
extraOptions: extraOptions, extraOptions: extraOptions,
gitDirectory: gitDirectory, gitDirectory: projectDirectory,
verbose: verbose verbose: verbose
) )
} }
@@ -129,7 +129,6 @@ extension SemVarOptions {
) throws -> Configuration.SemVar { ) throws -> Configuration.SemVar {
@Dependency(\.logger) var logger @Dependency(\.logger) var logger
// TODO: Update when / if there's an update config command.
if customCommand && preRelease.customPreRelease { if customCommand && preRelease.customPreRelease {
logger.warning(""" logger.warning("""
Custom pre-release can not be used at same time as custom command. Custom pre-release can not be used at same time as custom command.
@@ -167,6 +166,15 @@ extension ConfigurationOptions {
) )
} }
private func configurationToMerge(extraOptions: [String]) throws -> Configuration {
try .init(
target: target(),
strategy: semvarOptions.gitTag
? .semvar(semvarOptions(extraOptions: extraOptions))
: .branch(includeCommitSha: commitSha)
)
}
func shared( func shared(
command: String, command: String,
dryRun: Bool = true, dryRun: Bool = true,
@@ -177,12 +185,11 @@ extension ConfigurationOptions {
try .init( try .init(
allowPreReleaseTag: !semvarOptions.preRelease.disablePreRelease, allowPreReleaseTag: !semvarOptions.preRelease.disablePreRelease,
dryRun: dryRun, dryRun: dryRun,
gitDirectory: gitDirectory, projectDirectory: gitDirectory,
loggingOptions: .init(command: command, verbose: verbose), loggingOptions: .init(command: command, verbose: verbose),
target: target(), configurationToMerge: configurationToMerge(extraOptions: extraOptions),
branch: semvarOptions.gitTag ? nil : .init(includeCommitSha: commitSha), configurationFile: configurationFile,
semvar: semvarOptions(extraOptions: extraOptions), requireConfigurationFile: requireConfiguration
configurationFile: configurationFile
) )
} }
} }

View File

@@ -36,35 +36,46 @@ public struct CliClient: Sendable {
case major, minor, patch, preRelease case major, minor, patch, preRelease
} }
/// Represents options that are used by all the commands.
public struct SharedOptions: Equatable, Sendable { public struct SharedOptions: Equatable, Sendable {
/// Whether to allow pre-release suffixes.
let allowPreReleaseTag: Bool let allowPreReleaseTag: Bool
/// Flag on if we write to files or not.
let dryRun: Bool let dryRun: Bool
let gitDirectory: String?
/// Specify a path to the project directory.
let projectDirectory: String?
/// The logging options to use.
let loggingOptions: LoggingOptions let loggingOptions: LoggingOptions
let target: Configuration.Target?
let branch: Configuration.Branch? /// Configuration that gets merged with the loaded (or default) configuration.
let semvar: Configuration.SemVar? let configurationToMerge: Configuration?
/// Path to the configuration file to load.
let configurationFile: String? let configurationFile: String?
/// Fail if a configuration file is not found.
let requireConfigurationFile: Bool
public init( public init(
allowPreReleaseTag: Bool = true, allowPreReleaseTag: Bool = true,
dryRun: Bool = false, dryRun: Bool = false,
gitDirectory: String? = nil, projectDirectory: String? = nil,
loggingOptions: LoggingOptions, loggingOptions: LoggingOptions,
target: Configuration.Target? = nil, configurationToMerge: Configuration? = nil,
branch: Configuration.Branch? = nil, configurationFile: String? = nil,
semvar: Configuration.SemVar? = nil, requireConfigurationFile: Bool = false
configurationFile: String? = nil
) { ) {
self.allowPreReleaseTag = allowPreReleaseTag self.allowPreReleaseTag = allowPreReleaseTag
self.dryRun = dryRun self.dryRun = dryRun
self.gitDirectory = gitDirectory self.projectDirectory = projectDirectory
self.loggingOptions = loggingOptions self.loggingOptions = loggingOptions
self.target = target
self.branch = branch
self.semvar = semvar
self.configurationFile = configurationFile self.configurationFile = configurationFile
self.configurationToMerge = configurationToMerge
self.requireConfigurationFile = requireConfigurationFile
} }
} }

View File

@@ -37,7 +37,7 @@ to come after the plugin name.
| Option | Description | | Option | Description |
| ---------- | --------------------------------------------------------------------- | | ---------- | --------------------------------------------------------------------- |
| --dry-run | Do not write to any files, but describe where values would be written | | --print | Do not write to any files, but describe where values would be written |
| --filename | Override the file name to be written in the target directory | | --filename | Override the file name to be written in the target directory |
| --verbose | Increase the logging output | | --verbose | Increase the logging output |
@@ -47,7 +47,7 @@ to come after the plugin name.
swift package \ swift package \
--allow-writing-to-package-directory \ --allow-writing-to-package-directory \
generate-version \ generate-version \
--dry-run \ --print \
--verbose \ --verbose \
<target> <target>
``` ```

View File

@@ -25,14 +25,14 @@ public extension CliClient.SharedOptions {
logger.trace("\nConfiguration: \(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: projectDirectory)
logger.debug("Target: \(targetUrl.cleanFilePath)") logger.debug("Target: \(targetUrl.cleanFilePath)")
// Perform the operation, which generates the new version and writes it. // Perform the operation, which generates the new version and writes it.
try await operation( try await operation(
configuration.currentVersion( configuration.currentVersion(
targetUrl: targetUrl, targetUrl: targetUrl,
gitDirectory: gitDirectory gitDirectory: projectDirectory
) )
) )
@@ -50,27 +50,20 @@ public extension CliClient.SharedOptions {
@Dependency(\.configurationClient) var configurationClient @Dependency(\.configurationClient) var configurationClient
@Dependency(\.logger) var logger @Dependency(\.logger) var logger
var strategy: Configuration.VersionStrategy? if configurationToMerge?.strategy?.branch != nil {
if let branch {
logger.trace("Merging branch strategy.") logger.trace("Merging branch strategy.")
strategy = .branch(branch) // strategy = .branch(branch)
} else if let semvar { } else if let semvar = configurationToMerge?.strategy?.semvar {
logger.trace("Merging semvar strategy.") logger.trace("Merging semvar strategy.")
var semvarString = "" var semvarString = ""
customDump(semvar, to: &semvarString) customDump(semvar, to: &semvarString)
logger.trace("\(semvarString)") logger.trace("\(semvarString)")
strategy = .semvar(semvar)
} }
let configuration = Configuration(
target: target,
strategy: strategy
)
return try await configurationClient.withConfiguration( return try await configurationClient.withConfiguration(
path: configurationFile, path: configurationFile,
merging: configuration, merging: configurationToMerge,
strict: requireConfigurationFile,
operation: operation operation: operation
) )
} }
@@ -82,7 +75,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.info("\n\(string)\n") // logger.info("\n\(string)\n")
} }
} }
@@ -90,7 +83,11 @@ public extension CliClient.SharedOptions {
@Dependency(\.logger) var logger @Dependency(\.logger) var logger
let version = try currentVersion.version.string(allowPreReleaseTag: allowPreReleaseTag) let version = try currentVersion.version.string(allowPreReleaseTag: allowPreReleaseTag)
logger.debug("Version: \(version)") if !dryRun {
logger.debug("Version: \(version)")
} else {
logger.info("Version: \(version)")
}
let template = currentVersion.usesOptionalType ? Template.optional(version) : Template.nonOptional(version) let template = currentVersion.usesOptionalType ? Template.optional(version) : Template.nonOptional(version)
logger.trace("Template string: \(template)") logger.trace("Template string: \(template)")
@@ -160,6 +157,12 @@ extension CliClient.SharedOptions {
} }
logger.debug("Bumped version: \(version)") logger.debug("Bumped version: \(version)")
if dryRun {
logger.info("Version: \(version)")
return
}
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)
} }

View File

@@ -70,7 +70,6 @@ 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(
@@ -84,7 +83,6 @@ 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? {

View File

@@ -29,11 +29,26 @@ public struct ConfigurationClient: Sendable {
public var write: @Sendable (Configuration, URL) async throws -> Void public var write: @Sendable (Configuration, URL) async throws -> Void
/// Find a configuration file and load it if found. /// Find a configuration file and load it if found.
public func findAndLoad(_ url: URL? = nil) async throws -> Configuration { ///
/// - Parameters:
/// - url: The optional path to the configuration file.
/// - strict: Fail if a configuration file is not found, otherwise return default configuration.
public func findAndLoad(_ url: URL? = nil, strict: Bool = true) async throws -> Configuration {
guard let url = try? await find(url) else { guard let url = try? await find(url) else {
throw ConfigurationClientError.configurationNotFound if strict {
throw ConfigurationClientError.configurationNotFound
}
return .default
} }
return (try? await load(url)) ?? .default
let loaded = try? await load(url)
guard let loaded else {
if strict {
throw ConfigurationClientError.configurationNotFound
}
return .default
}
return loaded
} }
/// Loads configuration from the given path, or searches for the default file and loads it. /// Loads configuration from the given path, or searches for the default file and loads it.
@@ -47,10 +62,12 @@ public struct ConfigurationClient: Sendable {
public func withConfiguration<T>( public func withConfiguration<T>(
path: String?, path: String?,
merging other: Configuration? = nil, merging other: Configuration? = nil,
strict: Bool = true,
operation: (Configuration) async throws -> T operation: (Configuration) async throws -> T
) async throws -> T { ) async throws -> T {
let configuration = try await findAndLoad( let configuration = try await findAndLoad(
path != nil ? URL(filePath: path!) : nil path != nil ? URL(filePath: path!) : nil,
strict: strict
) )
return try await operation(configuration.merging(other)) return try await operation(configuration.merging(other))
} }

View File

@@ -138,11 +138,12 @@ extension CliClient.SharedOptions {
) -> Self { ) -> Self {
return .init( return .init(
dryRun: dryRun, dryRun: dryRun,
gitDirectory: gitDirectory, projectDirectory: gitDirectory,
loggingOptions: .init(command: "test", verbose: 2), loggingOptions: .init(command: "test", verbose: 2),
target: .init(module: .init(target)), configurationToMerge: .init(
branch: versionStrategy.branch, target: .init(module: .init(target)),
semvar: versionStrategy.semvar strategy: versionStrategy
)
) )
} }
} }

View File

@@ -1,71 +0,0 @@
@_spi(Internal) import CliClient
import Dependencies
import FileClient
import GitClient
import ShellClient
import TestSupport
import XCTest
// TODO: Remove
final class GitVersionTests: XCTestCase {
override func invokeTest() {
withDependencies({
$0.logger.logLevel = .debug
$0.logger = .liveValue
$0.asyncShellClient = .liveValue
$0.gitClient = .liveValue
$0.fileClient = .liveValue
}, operation: {
super.invokeTest()
})
}
var gitDir: String {
URL(fileURLWithPath: #file)
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
.cleanFilePath
}
// #if !os(Linux)
// func test_live() async throws {
// @Dependency(\.gitClient) var versionClient: GitClient
//
// let version = try await versionClient.currentVersion(in: gitDir)
// print("VERSION: \(version)")
// // can't really have a predictable result for the live client.
// XCTAssertNotEqual(version, "blob")
// }
// #endif
func test_file_client() async throws {
try await withTemporaryDirectory { tmpDir in
@Dependency(\.fileClient) var fileClient
let filePath = tmpDir.appendingPathComponent("blob.txt")
try await fileClient.write(string: "Blob", to: filePath)
let contents = try await fileClient.read(filePath)
.trimmingCharacters(in: .whitespacesAndNewlines)
XCTAssertEqual(contents, "Blob")
}
}
func test_file_client_with_string_path() async throws {
try await withTemporaryDirectory { tmpDir in
@Dependency(\.fileClient) var fileClient
let filePath = tmpDir.appendingPathComponent("blob.txt")
let fileString = filePath.cleanFilePath
try await fileClient.write(string: "Blob", to: fileString)
let contents = try await fileClient.read(fileString)
.trimmingCharacters(in: .whitespacesAndNewlines)
XCTAssertEqual(contents, "Blob")
}
}
}

View File

@@ -1,4 +1,5 @@
@_spi(Internal) import ConfigurationClient @_spi(Internal) import ConfigurationClient
import CustomDump
import Dependencies import Dependencies
import Foundation import Foundation
import Testing import Testing
@@ -91,8 +92,8 @@ struct ConfigurationClientTests {
let other = Configuration.VersionStrategy.semvar(.init( let other = Configuration.VersionStrategy.semvar(.init(
allowPreRelease: true, allowPreRelease: true,
preRelease: .init(prefix: "foo", strategy: .gitTag), preRelease: .init(prefix: "foo", strategy: .gitTag),
requireExistingFile: true, requireExistingFile: false,
requireExistingSemVar: true, requireExistingSemVar: false,
strategy: .gitTag() strategy: .gitTag()
)) ))
let merged = strategy1.merging(other) let merged = strategy1.merging(other)