feat: Working on command-line documentation.
All checks were successful
CI / Ubuntu (push) Successful in 2m52s
All checks were successful
CI / Ubuntu (push) Successful in 2m52s
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -21,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"),
|
||||||
@@ -32,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(
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import Dependencies
|
import Dependencies
|
||||||
import FileClient
|
import FileClient
|
||||||
import Foundation
|
import Foundation
|
||||||
import ShellClient
|
|
||||||
|
|
||||||
@_spi(Internal)
|
@_spi(Internal)
|
||||||
public extension Configuration {
|
public extension Configuration {
|
||||||
@@ -58,19 +57,15 @@ public extension Configuration.SemVar {
|
|||||||
@_spi(Internal)
|
@_spi(Internal)
|
||||||
public extension Configuration.VersionStrategy {
|
public extension Configuration.VersionStrategy {
|
||||||
func merging(_ other: Self?) -> Self {
|
func merging(_ other: Self?) -> Self {
|
||||||
if let otherBranch = other?.branch {
|
guard let other else { return self }
|
||||||
guard let branch else {
|
|
||||||
return .branch(otherBranch)
|
|
||||||
}
|
|
||||||
return .branch(branch.merging(otherBranch))
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let branch else {
|
switch other {
|
||||||
guard let semvar else {
|
case .branch:
|
||||||
return other ?? self
|
guard let branch else { return other }
|
||||||
}
|
return .branch(branch.merging(other.branch))
|
||||||
return .semvar(semvar.merging(other?.semvar))
|
case .semvar:
|
||||||
|
guard let semvar else { return other }
|
||||||
|
return .semvar(semvar.merging(other.semvar))
|
||||||
}
|
}
|
||||||
return .branch(branch.merging(other?.branch))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ 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,
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
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 commandName = "build"
|
||||||
|
|
||||||
static let configuration: CommandConfiguration = .init(
|
static let configuration: CommandConfiguration = .init(
|
||||||
commandName: Self.commandName,
|
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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +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 commandName = "bump"
|
||||||
|
|
||||||
static let configuration = CommandConfiguration(
|
static let configuration = CommandConfiguration(
|
||||||
commandName: Self.commandName,
|
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
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import ArgumentParser
|
import ArgumentParser
|
||||||
import CliClient
|
import CliClient
|
||||||
|
import CliDoc
|
||||||
import ConfigurationClient
|
import ConfigurationClient
|
||||||
import CustomDump
|
import CustomDump
|
||||||
import Dependencies
|
import Dependencies
|
||||||
@@ -7,9 +8,11 @@ import FileClient
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct ConfigCommand: AsyncParsableCommand {
|
struct ConfigCommand: AsyncParsableCommand {
|
||||||
|
static let commandName = "config"
|
||||||
|
|
||||||
static let configuration = CommandConfiguration(
|
static let configuration = CommandConfiguration(
|
||||||
commandName: "config",
|
commandName: commandName,
|
||||||
abstract: "Configuration commands",
|
abstract: Abstract.default("Configuration commands").render(),
|
||||||
subcommands: [
|
subcommands: [
|
||||||
DumpConfig.self,
|
DumpConfig.self,
|
||||||
GenerateConfig.self
|
GenerateConfig.self
|
||||||
@@ -19,19 +22,40 @@ struct ConfigCommand: AsyncParsableCommand {
|
|||||||
|
|
||||||
extension ConfigCommand {
|
extension ConfigCommand {
|
||||||
|
|
||||||
struct DumpConfig: AsyncParsableCommand {
|
struct DumpConfig: CommandRepresentable {
|
||||||
static let commandName = "dump"
|
static let commandName = "dump"
|
||||||
|
static let parentCommand = ConfigCommand.commandName
|
||||||
|
|
||||||
static let configuration = CommandConfiguration(
|
static let configuration = CommandConfiguration(
|
||||||
commandName: Self.commandName,
|
commandName: Self.commandName,
|
||||||
abstract: "Inspect the parsed configuration.",
|
abstract: Abstract.default("Inspect the parsed configuration."),
|
||||||
discussion: """
|
usage: Usage.default(parentCommand: ConfigCommand.commandName, commandName: Self.commandName),
|
||||||
This will load any configuration and merge the options passed in. Then print it to stdout.
|
discussion: Discussion.default(
|
||||||
The default style is to print the output in `swift`, however you can use the `--print` flag to
|
notes: [
|
||||||
print the output in `json`.
|
"""
|
||||||
""",
|
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"]
|
aliases: ["d"]
|
||||||
)
|
)
|
||||||
|
@Flag(
|
||||||
|
help: "Change the style of what get's printed."
|
||||||
|
)
|
||||||
|
fileprivate var printStyle: PrintStyle = .json
|
||||||
|
|
||||||
@OptionGroup var globals: ConfigCommandOptions
|
@OptionGroup var globals: ConfigCommandOptions
|
||||||
|
|
||||||
@@ -40,14 +64,28 @@ extension ConfigCommand {
|
|||||||
.shared(command: Self.commandName)
|
.shared(command: Self.commandName)
|
||||||
.runClient(\.parsedConfiguration)
|
.runClient(\.parsedConfiguration)
|
||||||
|
|
||||||
try globals.printConfiguration(configuration)
|
try globals.printConfiguration(configuration, style: printStyle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct GenerateConfig: AsyncParsableCommand {
|
struct GenerateConfig: CommandRepresentable {
|
||||||
|
static let commandName = "generate"
|
||||||
|
static let parentCommand = ConfigCommand.commandName
|
||||||
|
|
||||||
static let configuration: CommandConfiguration = .init(
|
static let configuration: CommandConfiguration = .init(
|
||||||
commandName: "generate",
|
commandName: commandName,
|
||||||
abstract: "Generate a configuration file.",
|
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"]
|
aliases: ["g"]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -56,6 +94,12 @@ extension ConfigCommand {
|
|||||||
)
|
)
|
||||||
var style: ConfigCommand.Style = .semvar
|
var style: ConfigCommand.Style = .semvar
|
||||||
|
|
||||||
|
@Flag(
|
||||||
|
name: .customLong("print"),
|
||||||
|
help: "Print json to stdout."
|
||||||
|
)
|
||||||
|
var printJson: Bool = false
|
||||||
|
|
||||||
@OptionGroup var globals: ConfigCommandOptions
|
@OptionGroup var globals: ConfigCommandOptions
|
||||||
|
|
||||||
func run() async throws {
|
func run() async throws {
|
||||||
@@ -67,7 +111,7 @@ extension ConfigCommand {
|
|||||||
extraOptions: globals.extraOptions
|
extraOptions: globals.extraOptions
|
||||||
)
|
)
|
||||||
|
|
||||||
switch globals.printJson {
|
switch printJson {
|
||||||
case true:
|
case true:
|
||||||
try globals.handlePrintJson(configuration)
|
try globals.handlePrintJson(configuration)
|
||||||
case false:
|
case false:
|
||||||
@@ -109,14 +153,14 @@ extension ConfigCommand {
|
|||||||
@dynamicMemberLookup
|
@dynamicMemberLookup
|
||||||
struct ConfigCommandOptions: ParsableArguments {
|
struct ConfigCommandOptions: ParsableArguments {
|
||||||
|
|
||||||
@Flag(
|
|
||||||
name: .customLong("print"),
|
|
||||||
help: "Print style to stdout."
|
|
||||||
)
|
|
||||||
var printJson: Bool = false
|
|
||||||
|
|
||||||
@OptionGroup var configOptions: ConfigurationOptions
|
@OptionGroup var configOptions: ConfigurationOptions
|
||||||
|
|
||||||
|
@Flag(
|
||||||
|
name: .shortAndLong,
|
||||||
|
help: "Increase logging level, can be passed multiple times (example: -vvv)."
|
||||||
|
)
|
||||||
|
var verbose: Int
|
||||||
|
|
||||||
@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
|
||||||
@@ -131,10 +175,16 @@ extension ConfigCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension ConfigCommand.DumpConfig {
|
||||||
|
enum PrintStyle: EnumerableFlag {
|
||||||
|
case json, swift
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private extension ConfigCommand.ConfigCommandOptions {
|
private extension ConfigCommand.ConfigCommandOptions {
|
||||||
|
|
||||||
func shared(command: String) throws -> CliClient.SharedOptions {
|
func shared(command: String) throws -> CliClient.SharedOptions {
|
||||||
try configOptions.shared(command: command, extraOptions: extraOptions, verbose: 2)
|
try configOptions.shared(command: command, extraOptions: extraOptions, verbose: verbose)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlePrintJson(_ configuration: Configuration) throws {
|
func handlePrintJson(_ configuration: Configuration) throws {
|
||||||
@@ -149,12 +199,22 @@ private extension ConfigCommand.ConfigCommandOptions {
|
|||||||
print(string)
|
print(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
func printConfiguration(_ configuration: Configuration) throws {
|
func printConfiguration(
|
||||||
guard printJson else {
|
_ configuration: Configuration,
|
||||||
|
style: ConfigCommand.DumpConfig.PrintStyle
|
||||||
|
) throws {
|
||||||
|
switch style {
|
||||||
|
case .json:
|
||||||
|
try handlePrintJson(configuration)
|
||||||
|
case .swift:
|
||||||
customDump(configuration)
|
customDump(configuration)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
try handlePrintJson(configuration)
|
|
||||||
|
// guard printJson else {
|
||||||
|
// customDump(configuration)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// try handlePrintJson(configuration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import ArgumentParser
|
import ArgumentParser
|
||||||
import CliClient
|
import CliClient
|
||||||
|
import CliDoc
|
||||||
import Dependencies
|
import Dependencies
|
||||||
import Foundation
|
import Foundation
|
||||||
import ShellClient
|
import ShellClient
|
||||||
@@ -9,8 +10,11 @@ struct GenerateCommand: AsyncParsableCommand {
|
|||||||
|
|
||||||
static let configuration: CommandConfiguration = .init(
|
static let configuration: CommandConfiguration = .init(
|
||||||
commandName: Self.commandName,
|
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.").render(),
|
||||||
discussion: "This command can be interacted with directly, outside of the plugin usage context."
|
usage: Usage.default(commandName: Self.commandName),
|
||||||
|
discussion: Discussion {
|
||||||
|
"This command can be interacted with directly, outside of the plugin usage context."
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@OptionGroup var globals: GlobalOptions
|
@OptionGroup var globals: GlobalOptions
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -41,7 +43,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.",
|
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?
|
||||||
|
|||||||
307
Sources/bump-version/Helpers/DocHelpers.swift
Normal file
307
Sources/bump-version/Helpers/DocHelpers.swift
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -167,7 +167,6 @@ extension ConfigurationOptions {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Need to potentially do something different with passing branch.
|
|
||||||
func shared(
|
func shared(
|
||||||
command: String,
|
command: String,
|
||||||
dryRun: Bool = true,
|
dryRun: Bool = true,
|
||||||
|
|||||||
@@ -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