feat: Adds git client tests, restructures files.
Some checks failed
CI / macOS (debug, 16.1) (push) Has been cancelled
CI / macOS (release, 16.1) (push) Has been cancelled
CI / Ubuntu (push) Failing after 5s

This commit is contained in:
2024-12-24 14:42:28 -05:00
parent 8aa4b73cab
commit 1972260317
26 changed files with 454 additions and 397 deletions

View File

@@ -2,16 +2,16 @@ import ArgumentParser
import Foundation
@main
struct CliVersionCommand: AsyncParsableCommand {
struct Application: AsyncParsableCommand {
static let configuration: CommandConfiguration = .init(
commandName: "cli-version",
commandName: "bump-version",
version: VERSION ?? "0.0.0",
subcommands: [
Build.self,
Bump.self,
Generate.self,
BuildCommand.self,
BumpCommand.self,
GenerateCommand.self,
UtilsCommand.self
],
defaultSubcommand: Bump.self
defaultSubcommand: BumpCommand.self
)
}

View File

@@ -1,19 +0,0 @@
import ArgumentParser
import CliClient
import Foundation
import ShellClient
extension CliVersionCommand {
struct Build: AsyncParsableCommand {
static let configuration: CommandConfiguration = .init(
abstract: "Used for the build with version plugin.",
discussion: "This should generally not be interacted with directly, outside of the build plugin."
)
@OptionGroup var globals: GlobalOptions
func run() async throws {
try await globals.shared().run(\.build)
}
}
}

View File

@@ -1,21 +0,0 @@
import ArgumentParser
import CliClient
import Dependencies
extension CliVersionCommand {
struct Bump: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "bump",
abstract: "Bump version of a command-line tool."
)
@OptionGroup var globals: GlobalOptions
func run() async throws {
try await globals.shared().run(\.bump, args: nil)
}
}
}
extension CliClient.BumpOption: EnumerableFlag {}

View File

@@ -0,0 +1,19 @@
import ArgumentParser
import CliClient
import Foundation
import ShellClient
struct BuildCommand: AsyncParsableCommand {
static let configuration: CommandConfiguration = .init(
commandName: "build",
abstract: "Used for the build with version plugin.",
discussion: "This should generally not be interacted with directly, outside of the build plugin.",
shouldDisplay: false
)
@OptionGroup var globals: GlobalOptions
func run() async throws {
try await globals.run(\.build)
}
}

View File

@@ -0,0 +1,26 @@
import ArgumentParser
import CliClient
import Dependencies
struct BumpCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "bump",
abstract: "Bump version of a command-line tool."
)
@OptionGroup var globals: GlobalOptions
@Flag(
help: """
The semvar bump option, this is ignored if the configuration is set to use a branch/commit sha strategy.
"""
)
var bumpOption: CliClient.BumpOption = .patch
func run() async throws {
try await globals.run(\.bump, args: bumpOption)
}
}
extension CliClient.BumpOption: EnumerableFlag {}

View File

@@ -0,0 +1,19 @@
import ArgumentParser
import CliClient
import Dependencies
import Foundation
import ShellClient
struct GenerateCommand: AsyncParsableCommand {
static let configuration: CommandConfiguration = .init(
commandName: "generate",
abstract: "Generates a version file in a command line tool that can be set via the git tag or git sha.",
discussion: "This command can be interacted with directly, outside of the plugin usage context."
)
@OptionGroup var globals: GlobalOptions
func run() async throws {
try await globals.run(\.generate)
}
}

View File

@@ -0,0 +1,95 @@
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
}
}

View File

@@ -1,61 +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))
// }
// }
//
// @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)
// }

View File

@@ -0,0 +1,5 @@
# Application
## Articles
- <doc:Installation>

View File

@@ -0,0 +1,3 @@
# Installation
You can install the command-line application by...

View File

@@ -1,20 +0,0 @@
import ArgumentParser
import CliClient
import Dependencies
import Foundation
import ShellClient
extension CliVersionCommand {
struct Generate: AsyncParsableCommand {
static let configuration: CommandConfiguration = .init(
abstract: "Generates a version file in a command line tool that can be set via the git tag or git sha.",
discussion: "This command can be interacted with directly, outside of the plugin usage context."
)
@OptionGroup var globals: GlobalOptions
func run() async throws {
try await globals.shared().run(\.generate)
}
}
}

View File

@@ -7,24 +7,8 @@ import Rainbow
struct GlobalOptions: ParsableArguments {
@Option(
name: .shortAndLong,
help: "Specify the path to a configuration file."
)
var configurationFile: String?
@OptionGroup var targetOptions: TargetOptions
@OptionGroup var semvarOptions: SemVarOptions
@Flag(
name: .long,
inversion: .prefixedNo,
help: """
Include the short commit sha in version or pre-release branch style output.
"""
)
var commitSha: Bool = true
@OptionGroup
var configOptions: ConfigurationOptions
@Option(
name: .customLong("git-directory"),
@@ -44,6 +28,37 @@ struct GlobalOptions: ParsableArguments {
)
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` flag is not set.
"""
)
var extraOptions: [String] = []
}
struct ConfigurationOptions: ParsableArguments {
@Option(
name: .shortAndLong,
help: "Specify the path to a configuration file.",
completion: .file(extensions: ["json"])
)
var configurationFile: String?
@OptionGroup var targetOptions: TargetOptions
@OptionGroup var semvarOptions: SemVarOptions
@Flag(
name: .long,
inversion: .prefixedNo,
help: """
Include the short commit sha in version or pre-release branch style output.
"""
)
var commitSha: Bool = true
}
struct TargetOptions: ParsableArguments {
@@ -67,8 +82,6 @@ struct TargetOptions: ParsableArguments {
}
// TODO: Need to be able to pass in arguments for custom command pre-release option.
struct PreReleaseOptions: ParsableArguments {
@Flag(
@@ -101,14 +114,13 @@ struct PreReleaseOptions: ParsableArguments {
)
var preReleasePrefix: String?
@Option(
@Flag(
name: .long,
help: """
Apply custom pre-release suffix, can also use branch or tag along with this
option as a prefix, used if branch is not set. (example: \"rc\")
Apply custom pre-release suffix, using extra options / arguments passed in after a '--'.
"""
)
var custom: String?
var customPreRelease: Bool = false
}
@@ -130,102 +142,3 @@ struct SemVarOptions: ParsableArguments {
@OptionGroup var preRelease: PreReleaseOptions
}
// TODO: Move these to global options.
extension CliClient.SharedOptions {
func run(_ keyPath: KeyPath<CliClient, @Sendable (Self) async throws -> String>) async throws {
try await withDependencies {
$0.fileClient = .liveValue
$0.gitClient = .liveValue
$0.cliClient = .liveValue
} operation: {
@Dependency(\.cliClient) var cliClient
let output = try await cliClient[keyPath: keyPath](self)
print(output)
}
}
func run<T>(
_ keyPath: KeyPath<CliClient, @Sendable (T, Self) async throws -> String>,
args: T
) async throws {
try await withDependencies {
$0.fileClient = .liveValue
$0.gitClient = .liveValue
$0.cliClient = .liveValue
} operation: {
@Dependency(\.cliClient) var cliClient
let output = try await cliClient[keyPath: keyPath](args, self)
print(output)
}
}
}
extension GlobalOptions {
func shared() throws -> CliClient.SharedOptions {
try .init(
allowPreReleaseTag: !semvarOptions.preRelease.disablePreRelease,
dryRun: dryRun,
gitDirectory: gitDirectory,
verbose: verbose,
target: targetOptions.configTarget(),
branch: .init(includeCommitSha: commitSha),
semvar: semvarOptions.configSemVarOptions(),
configurationFile: configurationFile
)
}
}
// MARK: - Helpers
private extension TargetOptions {
func configTarget() throws -> Configuration.Target? {
guard let path else {
guard let module else {
return nil
}
return .init(module: .init(module, fileName: fileName))
}
return .init(path: path)
}
}
extension PreReleaseOptions {
// FIX:
func configPreReleaseStrategy() throws -> Configuration.PreRelease? {
return nil
// guard let custom else {
// if useBranchAsPreRelease {
// return .branch()
// } else if useTagAsPreRelease {
// return .gitTag
// } else {
// return nil
// }
// }
//
// if useBranchAsPreRelease {
// return .customBranchPrefix(custom)
// } else if useTagAsPreRelease {
// return .customGitTagPrefix(custom)
// } else {
// return .custom(custom)
// }
}
}
extension SemVarOptions {
func configSemVarOptions() throws -> Configuration.SemVar {
try .init(
preRelease: preRelease.configPreReleaseStrategy(),
requireExistingFile: requireExistingFile,
requireExistingSemVar: requireExistingSemvar
)
}
}

View File

@@ -0,0 +1,123 @@
import CliClient
import ConfigurationClient
import Dependencies
import FileClient
import GitClient
@discardableResult
func withSetupDependencies<T>(
_ operation: () async throws -> T
) async throws -> T {
try await withDependencies {
$0.fileClient = .liveValue
$0.gitClient = .liveValue
$0.cliClient = .liveValue
$0.configurationClient = .liveValue
} operation: {
try await operation()
}
}
extension GlobalOptions {
func runClient<T>(
_ keyPath: KeyPath<CliClient, @Sendable (CliClient.SharedOptions) async throws -> T>
) async throws -> T {
try await withSetupDependencies {
@Dependency(\.cliClient) var cliClient
return try await cliClient[keyPath: keyPath](shared())
}
}
func runClient<A, T>(
_ keyPath: KeyPath<CliClient, @Sendable (A, CliClient.SharedOptions) async throws -> T>,
args: A
) async throws -> T {
try await withSetupDependencies {
@Dependency(\.cliClient) var cliClient
return try await cliClient[keyPath: keyPath](args, shared())
}
}
func run(
_ keyPath: KeyPath<CliClient, @Sendable (CliClient.SharedOptions) async throws -> String>
) async throws {
let output = try await runClient(keyPath)
print(output)
}
func run<T>(
_ keyPath: KeyPath<CliClient, @Sendable (T, CliClient.SharedOptions) async throws -> String>,
args: T
) async throws {
let output = try await runClient(keyPath, args: args)
print(output)
}
func shared() throws -> CliClient.SharedOptions {
try .init(
allowPreReleaseTag: !configOptions.semvarOptions.preRelease.disablePreRelease,
dryRun: dryRun,
gitDirectory: gitDirectory,
verbose: verbose,
target: configOptions.target(),
branch: .init(includeCommitSha: configOptions.commitSha),
semvar: configOptions.semvarOptions(extraOptions: extraOptions),
configurationFile: configOptions.configurationFile
)
}
}
private extension TargetOptions {
func configTarget() throws -> Configuration.Target? {
guard let path else {
guard let module else {
return nil
}
return .init(module: .init(module, fileName: fileName))
}
return .init(path: path)
}
}
extension PreReleaseOptions {
func configPreReleaseStrategy(includeCommitSha: Bool, extraOptions: [String]) throws -> Configuration.PreRelease? {
if useBranchAsPreRelease {
return .init(prefix: preReleasePrefix, strategy: .branch(includeCommitSha: includeCommitSha))
} else if useTagAsPreRelease {
return .init(prefix: preReleasePrefix, strategy: .gitTag)
} else if customPreRelease {
guard extraOptions.count > 0 else {
throw ExtraOptionsEmpty()
}
return .init(prefix: preReleasePrefix, strategy: .command(arguments: extraOptions))
}
return nil
}
}
extension SemVarOptions {
func configSemVarOptions(includeCommitSha: Bool, extraOptions: [String]) throws -> Configuration.SemVar {
try .init(
preRelease: preRelease.configPreReleaseStrategy(includeCommitSha: includeCommitSha, extraOptions: extraOptions),
requireExistingFile: requireExistingFile,
requireExistingSemVar: requireExistingSemvar
)
}
}
extension ConfigurationOptions {
func target() throws -> Configuration.Target? {
try targetOptions.configTarget()
}
func semvarOptions(extraOptions: [String]) throws -> Configuration.SemVar {
try semvarOptions.configSemVarOptions(includeCommitSha: commitSha, extraOptions: extraOptions)
}
}
struct ExtraOptionsEmpty: Error {}

View File

@@ -1,48 +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
]
)
}
extension UtilsCommand {
struct DumpConfig: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "dump-config",
abstract: "Show the parsed configuration."
)
@Argument(
help: """
Optional path to the configuration file, if not supplied will search the current directory
""",
completion: .file(extensions: ["toml", "json"])
)
var file: String?
func run() async throws {
try await withDependencies {
$0.fileClient = .liveValue
$0.configurationClient = .liveValue
} operation: {
@Dependency(\.configurationClient) var configurationClient
let configuration = try await configurationClient.findAndLoad(
file != nil ? URL(filePath: file!) : nil
)
customDump(configuration)
}
}
}
}