Compare commits
10 Commits
c07a0ef13b
...
1972260317
| Author | SHA1 | Date | |
|---|---|---|---|
|
1972260317
|
|||
|
8aa4b73cab
|
|||
|
f2a2374c4f
|
|||
|
7d23172c71
|
|||
|
11ec829a90
|
|||
|
2152388ee3
|
|||
|
0836dbd6da
|
|||
|
e2309f5635
|
|||
|
c7d349e3cc
|
|||
|
9b60ba0e6c
|
@@ -3,9 +3,7 @@ requireExistingFile = true
|
||||
requireExistingSemVar = true
|
||||
|
||||
[strategy.semvar.preRelease]
|
||||
prefix = 'rc'
|
||||
style = 'custom'
|
||||
usesGitTag = false
|
||||
style = 'branch'
|
||||
|
||||
[strategy.semvar.preRelease.branch]
|
||||
includeCommitSha = true
|
||||
12
.bump-version.json
Normal file
12
.bump-version.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"strategy" : {
|
||||
"branch" : {
|
||||
"includeCommitSha": true
|
||||
}
|
||||
},
|
||||
"target" : {
|
||||
"module" : {
|
||||
"name" : "cli-version"
|
||||
}
|
||||
}
|
||||
}
|
||||
29
.github/workflows/ci.yml
vendored
29
.github/workflows/ci.yml
vendored
@@ -7,13 +7,13 @@ on:
|
||||
jobs:
|
||||
mac:
|
||||
name: macOS
|
||||
runs-on: macos-12
|
||||
runs-on: macos-15
|
||||
strategy:
|
||||
matrix:
|
||||
xcode: ['14.2']
|
||||
xcode: ['16.1']
|
||||
config: ['debug', 'release']
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Select Xcode ${{ matrix.xcode }}
|
||||
run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app
|
||||
- name: Swift Version
|
||||
@@ -21,13 +21,16 @@ jobs:
|
||||
- name: Run ${{ matrix.xcode }} Tests
|
||||
run: make CONFIG=${{ matrix.config }} test-library
|
||||
|
||||
# ubuntu:
|
||||
# name: Ubuntu
|
||||
# runs-on: ubuntu-20.04
|
||||
# steps:
|
||||
# - uses: swift-actions/setup-swift@v1
|
||||
# with:
|
||||
# swift-version: 5.7
|
||||
# - uses: actions/checkout@v3
|
||||
# - name: Run Tests
|
||||
# run: make DOCKER_PLATFORM=linux/amd64 test-linux
|
||||
ubuntu:
|
||||
name: Ubuntu
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup just.
|
||||
uses: extractions/setup-just@v2
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Setup buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Run tests.
|
||||
run: just test-docker
|
||||
|
||||
2
Makefile
2
Makefile
@@ -1,6 +1,6 @@
|
||||
PLATFORM_MACOS = macOS
|
||||
CONFIG := debug
|
||||
DOCC_TARGET ?= CliVersion
|
||||
DOCC_TARGET ?= CliClient
|
||||
DOCC_BASEPATH = $(shell basename "$(PWD)")
|
||||
DOCC_DIR ?= ./docs
|
||||
SWIFT_VERSION ?= "5.10"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "b7e59cc29214df682ee1aa756371133e94660f8aabd03c193d455ba057ed5d17",
|
||||
"originHash" : "6ab0a9c883cfa1490d249a344074ad27369033fab78e1a90272ef07339a8c0ab",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "combine-schedulers",
|
||||
@@ -46,6 +46,15 @@
|
||||
"version" : "1.3.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-custom-dump",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-custom-dump.git",
|
||||
"state" : {
|
||||
"revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1",
|
||||
"version" : "1.3.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-dependencies",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -96,8 +105,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/m-housh/swift-shell-client.git",
|
||||
"state" : {
|
||||
"revision" : "ea819f41b87aa94e792f028a031f5db786e79a94",
|
||||
"version" : "0.2.1"
|
||||
"revision" : "d67f693f92428ef8adecb48c2b26ccac0d2f98cb",
|
||||
"version" : "0.2.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -109,15 +118,6 @@
|
||||
"version" : "600.0.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "tomlkit",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/LebJe/TOMLKit.git",
|
||||
"state" : {
|
||||
"revision" : "ec6198d37d495efc6acd4dffbd262cdca7ff9b3f",
|
||||
"version" : "0.6.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "xctest-dynamic-overlay",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
||||
@@ -9,29 +9,34 @@ let package = Package(
|
||||
],
|
||||
products: [
|
||||
.library(name: "CliClient", targets: ["CliClient"]),
|
||||
.library(name: "ConfigurationClient", targets: ["ConfigurationClient"]),
|
||||
.library(name: "FileClient", targets: ["FileClient"]),
|
||||
.library(name: "GitClient", targets: ["GitClient"]),
|
||||
.plugin(name: "BuildWithVersionPlugin", targets: ["BuildWithVersionPlugin"]),
|
||||
.plugin(name: "GenerateVersionPlugin", targets: ["GenerateVersionPlugin"]),
|
||||
.plugin(name: "UpdateVersionPlugin", targets: ["UpdateVersionPlugin"])
|
||||
],
|
||||
dependencies: [
|
||||
.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.0"),
|
||||
.package(url: "https://github.com/m-housh/swift-shell-client.git", from: "0.2.2"),
|
||||
.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-log.git", from: "1.6.2"),
|
||||
.package(url: "https://github.com/LebJe/TOMLKit.git", from: "0.5.0")
|
||||
.package(url: "https://github.com/pointfreeco/swift-custom-dump.git", from: "1.3.3")
|
||||
],
|
||||
targets: [
|
||||
.executableTarget(
|
||||
name: "cli-version",
|
||||
dependencies: [
|
||||
"CliClient",
|
||||
.product(name: "ArgumentParser", package: "swift-argument-parser")
|
||||
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
||||
.product(name: "CustomDump", package: "swift-custom-dump")
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "CliClient",
|
||||
dependencies: [
|
||||
"ConfigurationClient",
|
||||
"FileClient",
|
||||
"GitClient",
|
||||
.product(name: "Logging", package: "swift-log")
|
||||
@@ -45,9 +50,9 @@ let package = Package(
|
||||
name: "ConfigurationClient",
|
||||
dependencies: [
|
||||
"FileClient",
|
||||
.product(name: "CustomDump", package: "swift-custom-dump"),
|
||||
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||
.product(name: "DependenciesMacros", package: "swift-dependencies"),
|
||||
.product(name: "TOMLKit", package: "TOMLKit")
|
||||
.product(name: "DependenciesMacros", package: "swift-dependencies")
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
@@ -70,6 +75,10 @@ let package = Package(
|
||||
.product(name: "ShellClient", package: "swift-shell-client")
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "GitClientTests",
|
||||
dependencies: ["GitClient"]
|
||||
),
|
||||
.target(name: "TestSupport"),
|
||||
.plugin(
|
||||
name: "BuildWithVersionPlugin",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import ConfigurationClient
|
||||
import Dependencies
|
||||
import DependenciesMacros
|
||||
import FileClient
|
||||
@@ -26,68 +27,65 @@ public struct CliClient: Sendable {
|
||||
/// Generate a version file with an optional version that can be set manually.
|
||||
public var generate: @Sendable (SharedOptions) async throws -> String
|
||||
|
||||
/// Parse the configuration options.
|
||||
public var parsedConfiguration: @Sendable (SharedOptions) async throws -> Configuration
|
||||
|
||||
public enum BumpOption: Sendable, CaseIterable {
|
||||
case major, minor, patch, preRelease
|
||||
}
|
||||
|
||||
public enum PreReleaseStrategy: Equatable, Sendable {
|
||||
/// Use output of tag, with branch and commit sha.
|
||||
case branchAndCommit
|
||||
|
||||
/// Provide a custom pre-release tag.
|
||||
indirect case custom(String, PreReleaseStrategy? = nil)
|
||||
|
||||
/// Use the output of `git describe --tags`
|
||||
case tag
|
||||
}
|
||||
|
||||
public enum VersionStrategy: Equatable, Sendable {
|
||||
case branchAndCommit
|
||||
case semVar(SemVarOptions)
|
||||
|
||||
public struct SemVarOptions: Equatable, Sendable {
|
||||
let preReleaseStrategy: PreReleaseStrategy?
|
||||
let requireExistingFile: Bool
|
||||
let requireExistingSemVar: Bool
|
||||
|
||||
public init(
|
||||
preReleaseStrategy: PreReleaseStrategy? = nil,
|
||||
requireExistingFile: Bool = true,
|
||||
requireExistingSemVar: Bool = true
|
||||
) {
|
||||
self.preReleaseStrategy = preReleaseStrategy
|
||||
self.requireExistingFile = requireExistingFile
|
||||
self.requireExistingSemVar = requireExistingSemVar
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct SharedOptions: Equatable, Sendable {
|
||||
|
||||
let allowPreReleaseTag: Bool
|
||||
let dryRun: Bool
|
||||
let gitDirectory: String?
|
||||
let logLevel: Logger.Level
|
||||
let preReleaseStrategy: PreReleaseStrategy?
|
||||
let target: String
|
||||
let versionStrategy: VersionStrategy
|
||||
let target: Configuration.Target?
|
||||
let branch: Configuration.Branch?
|
||||
let semvar: Configuration.SemVar?
|
||||
let configurationFile: String?
|
||||
|
||||
public init(
|
||||
allowPreReleaseTag: Bool = true,
|
||||
dryRun: Bool = false,
|
||||
gitDirectory: String? = nil,
|
||||
logLevel: Logger.Level = .debug,
|
||||
preReleaseStrategy: PreReleaseStrategy? = nil,
|
||||
target: String,
|
||||
versionStrategy: VersionStrategy = .semVar(.init())
|
||||
target: Configuration.Target? = nil,
|
||||
branch: Configuration.Branch? = nil,
|
||||
semvar: Configuration.SemVar? = nil,
|
||||
configurationFile: String? = nil
|
||||
) {
|
||||
self.allowPreReleaseTag = allowPreReleaseTag
|
||||
self.dryRun = dryRun
|
||||
self.target = target
|
||||
self.gitDirectory = gitDirectory
|
||||
self.logLevel = logLevel
|
||||
self.preReleaseStrategy = preReleaseStrategy
|
||||
self.versionStrategy = versionStrategy
|
||||
self.target = target
|
||||
self.branch = branch
|
||||
self.semvar = semvar
|
||||
self.configurationFile = configurationFile
|
||||
}
|
||||
|
||||
var allowPreReleaseTag: Bool { preReleaseStrategy != nil }
|
||||
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,7 +97,10 @@ extension CliClient: DependencyKey {
|
||||
.init(
|
||||
build: { try await $0.build(environment) },
|
||||
bump: { try await $1.bump($0) },
|
||||
generate: { try await $0.generate() }
|
||||
generate: { try await $0.generate() },
|
||||
parsedConfiguration: { options in
|
||||
try await options.withMergedConfiguration { $0 }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,19 +7,19 @@ Learn how to integrate the plugins into your project
|
||||
Use the plugins by including as a package to your project and declaring in the `plugins` section of
|
||||
your target.
|
||||
|
||||
> Note: You must use swift-tools version 5.6 or greater for package plugins and
|
||||
> target `macOS(.v10_15)` or greater.
|
||||
> Note: You must use swift-tools version 5.6 or greater for package plugins and target `macOS(.v13)`
|
||||
> or greater.
|
||||
|
||||
```swift
|
||||
// swift-tools-version: 5.7
|
||||
// swift-tools-version: 5.10
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
platforms:[.macOS(.v10_15)],
|
||||
platforms:[.macOS(.v13)],
|
||||
dependencies: [
|
||||
...,
|
||||
.package(url: "https://github.com/m-housh/swift-cli-version.git", from: "0.1.0")
|
||||
.package(url: "https://github.com/m-housh/swift-cli-version.git", from: "0.2.0")
|
||||
],
|
||||
targets: [
|
||||
.executableTarget(
|
||||
@@ -33,8 +33,8 @@ let package = Package(
|
||||
)
|
||||
```
|
||||
|
||||
The above example uses the build tool plugin. The `BuildWithVersionPlugin` will give you access
|
||||
to a `VERSION` variable in your project that you can use to supply the version of the tool.
|
||||
The above example uses the build tool plugin. The `BuildWithVersionPlugin` will give you access to a
|
||||
`VERSION` variable in your project that you can use to supply the version of the tool.
|
||||
|
||||
### Example
|
||||
|
||||
@@ -60,9 +60,9 @@ not declared in your source files.
|
||||

|
||||
|
||||
> Note: If your `DerivedData` folder lives in a directory that is a mounted volume / or somewhere
|
||||
> that is not under your home folder then you may get build failures using the build tool
|
||||
> plugin, it will work if you build from the command line and pass the `--disable-sandbox` flag to the
|
||||
> build command or use one of the manual methods.
|
||||
> that is not under your home folder then you may get build failures using the build tool plugin, it
|
||||
> will work if you build from the command line and pass the `--disable-sandbox` flag to the build
|
||||
> command or use one of the manual methods.
|
||||
|
||||
## See Also
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# ``CliVersion``
|
||||
# CliClient
|
||||
|
||||
Derive a version for a command line tool from git tags or a git sha.
|
||||
Derive a version for a command-line tool from git tags or a git sha.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
@@ -9,15 +9,10 @@ Derive a version for a command line tool from git tags or a git sha.
|
||||
## Overview
|
||||
|
||||
This tool exposes several plugins that can be used to derive a version for a command line program at
|
||||
build time or by manually running the plugin. The version is derived from git tags and falling back to
|
||||
the branch and git sha if a tag is not set for the current worktree state.
|
||||
build time or by manually running the plugin. The version is derived from git tags and falling back
|
||||
to the branch and git sha if a tag is not set for the current worktree state.
|
||||
|
||||
## Articles
|
||||
|
||||
- <doc:GettingStarted>
|
||||
- <doc:ManualPlugins>
|
||||
|
||||
## Api
|
||||
|
||||
- ``FileClient``
|
||||
- ``GitVersionClient``
|
||||
|
||||
70
Sources/CliClient/Internal/Configuration+merging.swift
Normal file
70
Sources/CliClient/Internal/Configuration+merging.swift
Normal file
@@ -0,0 +1,70 @@
|
||||
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)
|
||||
}
|
||||
195
Sources/CliClient/Internal/ConfigurationExtensions.swift
Normal file
195
Sources/CliClient/Internal/ConfigurationExtensions.swift
Normal file
@@ -0,0 +1,195 @@
|
||||
import ConfigurationClient
|
||||
import Dependencies
|
||||
import Foundation
|
||||
import GitClient
|
||||
|
||||
extension Configuration {
|
||||
func targetUrl(gitDirectory: String?) throws -> URL {
|
||||
guard let target else {
|
||||
throw ConfigurationParsingError.targetNotFound
|
||||
}
|
||||
return try target.url(gitDirectory: gitDirectory)
|
||||
}
|
||||
|
||||
func currentVersion(targetUrl: URL, gitDirectory: String?) async throws -> CurrentVersionContainer {
|
||||
guard let strategy else {
|
||||
throw ConfigurationParsingError.versionNotFound
|
||||
}
|
||||
return try await strategy.currentVersion(
|
||||
targetUrl: targetUrl,
|
||||
gitDirectory: gitDirectory
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension Configuration.Target {
|
||||
func url(gitDirectory: String?) throws -> URL {
|
||||
@Dependency(\.logger) var logger
|
||||
|
||||
let filePath: String
|
||||
|
||||
if let path {
|
||||
filePath = path
|
||||
} else {
|
||||
guard let module else {
|
||||
throw ConfigurationParsingError.pathOrModuleNotSet
|
||||
}
|
||||
|
||||
var path = module.name
|
||||
logger.debug("module.name: \(path)")
|
||||
|
||||
if path.hasPrefix("./") {
|
||||
path = String(path.dropFirst(2))
|
||||
}
|
||||
|
||||
if !path.hasPrefix("Sources") {
|
||||
logger.debug("no prefix")
|
||||
path = "Sources/\(path)"
|
||||
}
|
||||
|
||||
filePath = "\(path)/\(module.fileName)"
|
||||
}
|
||||
|
||||
if let gitDirectory {
|
||||
return URL(filePath: "\(gitDirectory)/\(filePath)")
|
||||
}
|
||||
return URL(filePath: filePath)
|
||||
}
|
||||
}
|
||||
|
||||
extension GitClient {
|
||||
func version(branch: Configuration.Branch, gitDirectory: String?) async throws -> String {
|
||||
@Dependency(\.gitClient) var gitClient
|
||||
|
||||
return try await gitClient.version(.init(
|
||||
gitDirectory: gitDirectory,
|
||||
style: .branch(commitSha: branch.includeCommitSha)
|
||||
)).description
|
||||
}
|
||||
}
|
||||
|
||||
extension Configuration.PreRelease {
|
||||
|
||||
func preReleaseString(gitDirectory: String?) async throws -> String {
|
||||
@Dependency(\.gitClient) var gitClient
|
||||
|
||||
let preReleaseString: String
|
||||
|
||||
if let branch = strategy?.branch {
|
||||
preReleaseString = try await gitClient.version(branch: branch, gitDirectory: gitDirectory)
|
||||
} else {
|
||||
preReleaseString = try await gitClient.version(.init(
|
||||
gitDirectory: gitDirectory,
|
||||
style: .tag(exactMatch: false)
|
||||
)).description
|
||||
}
|
||||
|
||||
if let prefix {
|
||||
return "\(prefix)-\(preReleaseString)"
|
||||
}
|
||||
return preReleaseString
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(Internal)
|
||||
public extension Configuration.SemVar {
|
||||
|
||||
private func applyingPreRelease(_ semVar: SemVar, _ gitDirectory: String?) async throws -> SemVar {
|
||||
@Dependency(\.logger) var logger
|
||||
logger.trace("Start apply pre-release to: \(semVar)")
|
||||
guard let preReleaseStrategy = self.preRelease else {
|
||||
logger.trace("No pre-release strategy, returning original semvar.")
|
||||
return semVar
|
||||
}
|
||||
|
||||
let preRelease = try await preReleaseStrategy.preReleaseString(gitDirectory: gitDirectory)
|
||||
logger.trace("Pre-release string: \(preRelease)")
|
||||
|
||||
return semVar.applyingPreRelease(preRelease)
|
||||
}
|
||||
|
||||
func currentVersion(file: URL, gitDirectory: String? = nil) async throws -> CurrentVersionContainer.Version {
|
||||
@Dependency(\.fileClient) var fileClient
|
||||
@Dependency(\.gitClient) var gitClient
|
||||
@Dependency(\.logger) var logger
|
||||
|
||||
let fileOutput = try? await fileClient.semVar(file: file, gitDirectory: gitDirectory)
|
||||
var semVar = fileOutput?.semVar
|
||||
|
||||
logger.trace("file output semvar: \(String(describing: semVar))")
|
||||
|
||||
let usesOptionalType = fileOutput?.usesOptionalType
|
||||
|
||||
// We parsed a semvar from the existing file, use it.
|
||||
if semVar != nil {
|
||||
return try await .semVar(
|
||||
applyingPreRelease(semVar!, gitDirectory),
|
||||
usesOptionalType: usesOptionalType ?? false
|
||||
)
|
||||
}
|
||||
|
||||
if requireExistingFile {
|
||||
logger.debug("Failed to parse existing file, and caller requires it.")
|
||||
throw CliClientError.fileDoesNotExist(path: file.cleanFilePath)
|
||||
}
|
||||
|
||||
logger.trace("Does not require existing file, checking git-tag.")
|
||||
|
||||
// Didn't have existing semVar loaded from file, so check for git-tag.
|
||||
semVar = try await gitClient.version(.init(
|
||||
gitDirectory: gitDirectory,
|
||||
style: .tag(exactMatch: false)
|
||||
)).semVar
|
||||
|
||||
if semVar != nil {
|
||||
return try await .semVar(
|
||||
applyingPreRelease(semVar!, gitDirectory),
|
||||
usesOptionalType: usesOptionalType ?? false
|
||||
)
|
||||
}
|
||||
|
||||
if requireExistingSemVar {
|
||||
logger.trace("Caller requires existing semvar and it was not found in file or git-tag.")
|
||||
throw CliClientError.semVarNotFound
|
||||
}
|
||||
|
||||
// Semvar doesn't exist, so create a new one.
|
||||
logger.trace("Generating new semvar.")
|
||||
return try await .semVar(
|
||||
applyingPreRelease(.init(), gitDirectory),
|
||||
usesOptionalType: usesOptionalType ?? false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension Configuration.VersionStrategy {
|
||||
|
||||
func currentVersion(targetUrl: URL, gitDirectory: String?) async throws -> CurrentVersionContainer {
|
||||
@Dependency(\.gitClient) var gitClient
|
||||
|
||||
guard let branch else {
|
||||
guard let semvar else {
|
||||
throw ConfigurationParsingError.versionStrategyError(
|
||||
message: "Neither branch nor semvar set on configuration."
|
||||
)
|
||||
}
|
||||
return try await .init(
|
||||
targetUrl: targetUrl,
|
||||
version: semvar.currentVersion(file: targetUrl, gitDirectory: gitDirectory)
|
||||
)
|
||||
}
|
||||
return try await .init(
|
||||
targetUrl: targetUrl,
|
||||
version: .string(
|
||||
gitClient.version(branch: branch, gitDirectory: gitDirectory)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enum ConfigurationParsingError: Error {
|
||||
case targetNotFound
|
||||
case pathOrModuleNotSet
|
||||
case versionStrategyError(message: String)
|
||||
case versionNotFound
|
||||
}
|
||||
@@ -34,6 +34,7 @@ public extension FileClient {
|
||||
guard let versionString else {
|
||||
throw CliClientError.failedToParseVersionFile
|
||||
}
|
||||
logger.debug("Parsed version string: \(versionString)")
|
||||
return (String(versionString), isOptional)
|
||||
}
|
||||
|
||||
@@ -41,8 +42,11 @@ public extension FileClient {
|
||||
file: URL,
|
||||
gitDirectory: String?
|
||||
) async throws -> (semVar: SemVar?, usesOptionalType: Bool) {
|
||||
@Dependency(\.logger) var logger
|
||||
let (string, usesOptionalType) = try await getVersionString(fileUrl: file, gitDirectory: gitDirectory)
|
||||
return (SemVar(string: string), usesOptionalType)
|
||||
let semvar = SemVar(string: string)
|
||||
logger.debug("Semvar: \(String(describing: semvar))")
|
||||
return (semvar, usesOptionalType)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import ConfigurationClient
|
||||
import Dependencies
|
||||
import FileClient
|
||||
import Foundation
|
||||
@@ -6,28 +7,23 @@ import GitClient
|
||||
@_spi(Internal)
|
||||
public extension CliClient.SharedOptions {
|
||||
|
||||
func parseTargetUrl() async throws -> URL {
|
||||
@Dependency(\.fileClient) var fileClient
|
||||
// Merges any configuration set via the passed in options.
|
||||
@discardableResult
|
||||
func withMergedConfiguration<T>(
|
||||
operation: (Configuration) async throws -> T
|
||||
) async throws -> T {
|
||||
try await withConfiguration(path: configurationFile) { configuration in
|
||||
var configuration = configuration
|
||||
configuration = configuration.mergingTarget(target)
|
||||
|
||||
let target = target.hasPrefix(".") ? String(target.dropFirst()) : target
|
||||
let targetHasSources = target.hasPrefix("Sources") || target.hasPrefix("/Sources")
|
||||
|
||||
var url = url(for: gitDirectory ?? (targetHasSources ? target : "Sources"))
|
||||
|
||||
if gitDirectory != nil {
|
||||
if !targetHasSources {
|
||||
url.appendPathComponent("Sources")
|
||||
}
|
||||
url.appendPathComponent(target)
|
||||
if configuration.strategy?.branch != nil, let branch {
|
||||
configuration = configuration.mergingStrategy(.branch(branch))
|
||||
} else if let semvar {
|
||||
configuration = configuration.mergingStrategy(.semvar(semvar))
|
||||
}
|
||||
|
||||
let isDirectory = try await fileClient.isDirectory(url.cleanFilePath)
|
||||
|
||||
if isDirectory {
|
||||
url.appendPathComponent(Constants.defaultFileName)
|
||||
return try await operation(configuration)
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
@@ -37,21 +33,29 @@ public extension CliClient.SharedOptions {
|
||||
try await withDependencies {
|
||||
$0.logger.logLevel = logLevel
|
||||
} operation: {
|
||||
// Load the default configuration, if it exists.
|
||||
try await withMergedConfiguration { configuration in
|
||||
@Dependency(\.logger) var logger
|
||||
|
||||
let targetUrl = try await parseTargetUrl()
|
||||
logger.debug("Configuration: \(configuration)")
|
||||
|
||||
// This will fail if the target url is not set properly.
|
||||
let targetUrl = try configuration.targetUrl(gitDirectory: gitDirectory)
|
||||
logger.debug("Target: \(targetUrl.cleanFilePath)")
|
||||
|
||||
// Perform the operation, which generates the new version and writes it.
|
||||
try await operation(
|
||||
versionStrategy.currentVersion(
|
||||
file: targetUrl,
|
||||
configuration.currentVersion(
|
||||
targetUrl: targetUrl,
|
||||
gitDirectory: gitDirectory
|
||||
)
|
||||
)
|
||||
|
||||
// Return the file path we wrote the version to.
|
||||
return targetUrl.cleanFilePath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func write(_ string: String, to url: URL) async throws {
|
||||
@Dependency(\.fileClient) var fileClient
|
||||
@@ -124,9 +128,11 @@ extension CliClient.SharedOptions {
|
||||
|
||||
switch container.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.")
|
||||
try await write(container)
|
||||
|
||||
case let .semVar(semVar, usesOptionalType: usesOptionalType):
|
||||
logger.debug("Semvar prior to bumping: \(semVar)")
|
||||
let bumped = semVar.bump(type, preRelease: nil) // preRelease is already set on semVar.
|
||||
let version = bumped.versionString(withPreReleaseTag: allowPreReleaseTag)
|
||||
logger.debug("Bumped version: \(version)")
|
||||
|
||||
@@ -37,16 +37,30 @@ public struct SemVar: CustomStringConvertible, Equatable, Sendable {
|
||||
}
|
||||
|
||||
public init?(string: String) {
|
||||
@Dependency(\.logger) var logger
|
||||
|
||||
logger.trace("Parsing semvar from: \(string)")
|
||||
|
||||
let parts = string.split(separator: ".")
|
||||
logger.trace("parts: \(parts)")
|
||||
guard parts.count >= 3 else {
|
||||
return nil
|
||||
}
|
||||
let major = Int(String(parts[0].replacingOccurrences(of: "\"", with: "")))
|
||||
let minor = Int(String(parts[1]))
|
||||
|
||||
let patchParts = parts[2].split(separator: "-")
|
||||
let major = Int(String(parts[0].replacingOccurrences(of: "\"", with: "")))
|
||||
logger.trace("major: \(String(describing: major))")
|
||||
|
||||
let minor = Int(String(parts[1]))
|
||||
logger.trace("minor: \(String(describing: minor))")
|
||||
|
||||
let patchParts = parts[2].replacingOccurrences(of: "\"", with: "").split(separator: "-")
|
||||
logger.trace("patchParts: \(String(describing: patchParts))")
|
||||
|
||||
let patch = Int(patchParts.first ?? "0")
|
||||
logger.trace("patch: \(String(describing: patch))")
|
||||
|
||||
let preRelease = String(patchParts.dropFirst().joined(separator: "-"))
|
||||
logger.trace("preRelease: \(String(describing: preRelease))")
|
||||
|
||||
self.init(
|
||||
major: major ?? 0,
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import Dependencies
|
||||
import FileClient
|
||||
import struct Foundation.URL
|
||||
import GitClient
|
||||
|
||||
@_spi(Internal)
|
||||
public extension CliClient.PreReleaseStrategy {
|
||||
|
||||
func preReleaseString(gitDirectory: String?) async throws -> String {
|
||||
@Dependency(\.gitClient) var gitClient
|
||||
switch self {
|
||||
case let .custom(string, child):
|
||||
guard let child else { return string }
|
||||
return try await "\(string)-\(child.preReleaseString(gitDirectory: gitDirectory))"
|
||||
case .tag:
|
||||
return try await gitClient.version(.init(
|
||||
gitDirectory: gitDirectory,
|
||||
style: .tag(exactMatch: false)
|
||||
)).description
|
||||
case .branchAndCommit:
|
||||
return try await gitClient.version(.init(
|
||||
gitDirectory: gitDirectory,
|
||||
style: .branch(commitSha: true)
|
||||
)).description
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(Internal)
|
||||
public extension CliClient.VersionStrategy.SemVarOptions {
|
||||
|
||||
private func applyingPreRelease(_ semVar: SemVar, _ gitDirectory: String?) async throws -> SemVar {
|
||||
guard let preReleaseStrategy else { return semVar }
|
||||
let preRelease = try await preReleaseStrategy.preReleaseString(gitDirectory: gitDirectory)
|
||||
return semVar.applyingPreRelease(preRelease)
|
||||
}
|
||||
|
||||
func currentVersion(file: URL, gitDirectory: String? = nil) async throws -> CurrentVersionContainer.Version {
|
||||
@Dependency(\.fileClient) var fileClient
|
||||
@Dependency(\.gitClient) var gitClient
|
||||
|
||||
let fileOutput = try? await fileClient.semVar(file: file, gitDirectory: gitDirectory)
|
||||
var semVar = fileOutput?.semVar
|
||||
let usesOptionalType = fileOutput?.usesOptionalType
|
||||
|
||||
if requireExistingFile {
|
||||
guard let semVar else {
|
||||
throw CliClientError.fileDoesNotExist(path: file.cleanFilePath)
|
||||
}
|
||||
return try await .semVar(
|
||||
applyingPreRelease(semVar, gitDirectory),
|
||||
usesOptionalType: usesOptionalType ?? false
|
||||
)
|
||||
}
|
||||
|
||||
// Didn't have existing semVar loaded from file, so check for git-tag.
|
||||
|
||||
semVar = try await gitClient.version(.init(
|
||||
gitDirectory: gitDirectory,
|
||||
style: .tag(exactMatch: false)
|
||||
)).semVar
|
||||
if requireExistingSemVar {
|
||||
guard let semVar else {
|
||||
fatalError()
|
||||
}
|
||||
return try await .semVar(
|
||||
applyingPreRelease(semVar, gitDirectory),
|
||||
usesOptionalType: usesOptionalType ?? false
|
||||
)
|
||||
}
|
||||
|
||||
return try await .semVar(
|
||||
applyingPreRelease(.init(), gitDirectory),
|
||||
usesOptionalType: usesOptionalType ?? false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(Internal)
|
||||
public extension CliClient.VersionStrategy {
|
||||
|
||||
func currentVersion(file: URL, gitDirectory: String?) async throws -> CurrentVersionContainer {
|
||||
@Dependency(\.gitClient) var gitClient
|
||||
|
||||
switch self {
|
||||
case .branchAndCommit:
|
||||
return try await .init(
|
||||
targetUrl: file,
|
||||
version: .string(
|
||||
gitClient.version(.init(
|
||||
gitDirectory: gitDirectory,
|
||||
style: .branch(commitSha: true)
|
||||
)).description
|
||||
)
|
||||
)
|
||||
case let .semVar(options):
|
||||
return try await .init(
|
||||
targetUrl: file,
|
||||
version: options.currentVersion(file: file, gitDirectory: gitDirectory)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import Dependencies
|
||||
import DependenciesMacros
|
||||
import Foundation
|
||||
import TOMLKit
|
||||
|
||||
public extension DependencyValues {
|
||||
var coders: Coders {
|
||||
@@ -14,17 +13,13 @@ public extension DependencyValues {
|
||||
public struct Coders: Sendable {
|
||||
public var jsonDecoder: @Sendable () -> JSONDecoder = { .init() }
|
||||
public var jsonEncoder: @Sendable () -> JSONEncoder = { .init() }
|
||||
public var tomlDecoder: @Sendable () -> TOMLDecoder = { .init() }
|
||||
public var tomlEncoder: @Sendable () -> TOMLEncoder = { .init() }
|
||||
}
|
||||
|
||||
extension Coders: DependencyKey {
|
||||
public static var testValue: Coders {
|
||||
.init(
|
||||
jsonDecoder: { .init() },
|
||||
jsonEncoder: { defaultJsonEncoder },
|
||||
tomlDecoder: { .init() },
|
||||
tomlEncoder: { .init() }
|
||||
jsonEncoder: { defaultJsonEncoder }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import CustomDump
|
||||
import Foundation
|
||||
import TOMLKit
|
||||
|
||||
/// Represents configuration that can be set via a file, generally in the root of the
|
||||
/// project directory.
|
||||
@@ -15,24 +15,26 @@ public struct Configuration: Codable, Equatable, Sendable {
|
||||
|
||||
public init(
|
||||
target: Target? = nil,
|
||||
strategy: VersionStrategy? = .init(semvar: .init())
|
||||
strategy: VersionStrategy? = .semvar(.init())
|
||||
) {
|
||||
self.target = target
|
||||
self.strategy = strategy
|
||||
}
|
||||
|
||||
public static var mock: Self {
|
||||
public static var `default`: Self { .mock(module: "<my-tool>") }
|
||||
|
||||
public static func mock(module: String = "cli-version") -> Self {
|
||||
.init(
|
||||
target: .init(module: .init("cli-version")),
|
||||
strategy: .init()
|
||||
target: .init(module: .init(module)),
|
||||
strategy: .semvar(.init())
|
||||
)
|
||||
}
|
||||
|
||||
public static var customPreRelease: Self {
|
||||
.init(
|
||||
target: .init(module: .init("cli-version")),
|
||||
strategy: .init(semvar: .init(
|
||||
preRelease: .customBranchPrefix("rc")
|
||||
strategy: .semvar(.init(
|
||||
preRelease: .init(prefix: "rc", strategy: .branch())
|
||||
))
|
||||
)
|
||||
}
|
||||
@@ -42,12 +44,12 @@ public extension Configuration {
|
||||
|
||||
/// Represents a branch version or pre-release strategy.
|
||||
///
|
||||
/// This derives the version from the branch name and short version
|
||||
/// of the commit sha if configured.
|
||||
/// This derives the version or pre-release suffix from the branch name and
|
||||
/// optionally the short version of the commit sha.
|
||||
struct Branch: Codable, Equatable, Sendable {
|
||||
|
||||
/// Include the commit sha in the output for this strategy.
|
||||
let includeCommitSha: Bool
|
||||
public let includeCommitSha: Bool
|
||||
|
||||
/// Create a new branch strategy.
|
||||
///
|
||||
@@ -63,67 +65,29 @@ public extension Configuration {
|
||||
/// This appends a suffix to the version that get's generated from the version strategy.
|
||||
/// For example: `1.0.0-rc-1`
|
||||
///
|
||||
struct PreReleaseStrategy: Codable, Equatable, Sendable {
|
||||
struct PreRelease: Codable, Equatable, Sendable {
|
||||
|
||||
/// Use branch and commit sha as pre-release suffix.
|
||||
public let branch: Branch?
|
||||
|
||||
/// Use a custom prefix string.
|
||||
public let prefix: String?
|
||||
public let strategy: Strategy?
|
||||
|
||||
/// An identifier for the type of pre-release.
|
||||
public let style: StyleId
|
||||
|
||||
/// Whether we use `git describe --tags` for part of the suffix, this is only used
|
||||
/// if we have a custom style.
|
||||
public let usesGitTag: Bool
|
||||
|
||||
init(
|
||||
style: StyleId,
|
||||
branch: Branch? = nil,
|
||||
public init(
|
||||
prefix: String? = nil,
|
||||
usesGitTag: Bool = false
|
||||
strategy: Strategy? = nil
|
||||
) {
|
||||
self.branch = branch
|
||||
self.prefix = prefix
|
||||
self.style = style
|
||||
self.usesGitTag = usesGitTag
|
||||
self.strategy = strategy
|
||||
}
|
||||
|
||||
/// Represents a pre-release strategy that is derived from calling
|
||||
/// `git describe --tags`.
|
||||
public static let gitTag = Self(style: StyleId.gitTag)
|
||||
|
||||
/// Represents a pre-release strategy that is derived from the branch and commit sha.
|
||||
public static func branch(_ branch: Branch = .init()) -> Self {
|
||||
.init(style: .branch, branch: branch)
|
||||
}
|
||||
|
||||
/// Represents a custom strategy that uses the given value, not deriving any other
|
||||
/// data.
|
||||
public static func custom(_ prefix: String) -> Self {
|
||||
.init(style: .custom, prefix: prefix)
|
||||
}
|
||||
|
||||
/// Represents a custom strategy that uses a prefix along with the branch and
|
||||
/// commit sha.
|
||||
public static func customBranchPrefix(
|
||||
_ prefix: String,
|
||||
branch: Branch = .init()
|
||||
) -> Self {
|
||||
.init(style: .custom, branch: branch, prefix: prefix)
|
||||
}
|
||||
|
||||
/// Represents a custom strategy that uses a prefix along with the output from
|
||||
/// calling `git describe --tags`.
|
||||
public static func customGitTagPrefix(_ prefix: String) -> Self {
|
||||
.init(style: StyleId.custom, prefix: prefix, usesGitTag: true)
|
||||
}
|
||||
|
||||
public enum StyleId: String, Codable, Sendable {
|
||||
case branch
|
||||
case custom
|
||||
public enum Strategy: Codable, Equatable, Sendable {
|
||||
case branch(includeCommitSha: Bool = true)
|
||||
case command(arguments: [String])
|
||||
case gitTag
|
||||
|
||||
public var branch: Branch? {
|
||||
guard case let .branch(includeCommitSha) = self
|
||||
else { return nil }
|
||||
return .init(includeCommitSha: includeCommitSha)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +98,7 @@ public extension Configuration {
|
||||
struct SemVar: Codable, Equatable, Sendable {
|
||||
|
||||
/// Optional pre-releas suffix strategy.
|
||||
public let preRelease: PreReleaseStrategy?
|
||||
public let preRelease: PreRelease?
|
||||
|
||||
/// Fail if an existing version file does not exist in the target.
|
||||
public let requireExistingFile: Bool
|
||||
@@ -143,7 +107,7 @@ public extension Configuration {
|
||||
public let requireExistingSemVar: Bool
|
||||
|
||||
public init(
|
||||
preRelease: PreReleaseStrategy? = nil,
|
||||
preRelease: PreRelease? = nil,
|
||||
requireExistingFile: Bool = true,
|
||||
requireExistingSemVar: Bool = true
|
||||
) {
|
||||
@@ -158,7 +122,7 @@ public extension Configuration {
|
||||
///
|
||||
/// This can either be a path to a version file or a module used to
|
||||
/// locate the version file.
|
||||
struct Target: Codable, Equatable, Sendable {
|
||||
struct Target: Codable, Equatable, Sendable, CustomDumpReflectable {
|
||||
|
||||
/// The path to a version file.
|
||||
public let path: String?
|
||||
@@ -206,6 +170,27 @@ public extension Configuration {
|
||||
self.name = name
|
||||
self.fileName = fileName
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public var customDumpMirror: Mirror {
|
||||
guard let module else {
|
||||
guard let path else { return .init(reflecting: self) }
|
||||
return .init(
|
||||
self,
|
||||
children: [
|
||||
"path": path
|
||||
],
|
||||
displayStyle: .struct
|
||||
)
|
||||
}
|
||||
return .init(
|
||||
self,
|
||||
children: [
|
||||
"module": module
|
||||
],
|
||||
displayStyle: .struct
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,31 +199,52 @@ public extension Configuration {
|
||||
/// Typically a `SemVar` strategy or `Branch`.
|
||||
///
|
||||
///
|
||||
struct VersionStrategy: Codable, Equatable, Sendable {
|
||||
enum VersionStrategy: Codable, Equatable, Sendable, CustomDumpReflectable {
|
||||
case branch(includeCommitSha: Bool = true)
|
||||
|
||||
/// Set if we're using the branch and commit sha to derive the version.
|
||||
let branch: Branch?
|
||||
case semvar(
|
||||
preRelease: PreRelease? = nil,
|
||||
requireExistingFile: Bool? = nil,
|
||||
requireExistingSemVar: Bool? = nil
|
||||
)
|
||||
|
||||
/// Set if we're using semvar to derive the version.
|
||||
let semvar: SemVar?
|
||||
public var branch: Branch? {
|
||||
guard case let .branch(includeCommitSha) = self
|
||||
else { return nil }
|
||||
|
||||
/// Create a new version strategy that uses branch and commit sha to derive the version.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - branch: The branch strategy options.
|
||||
public init(branch: Branch) {
|
||||
self.branch = branch
|
||||
self.semvar = nil
|
||||
return .init(includeCommitSha: includeCommitSha)
|
||||
}
|
||||
|
||||
/// Create a new version strategy that uses semvar to derive the version.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - semvar: The semvar strategy options.
|
||||
public init(semvar: SemVar = .init()) {
|
||||
self.branch = nil
|
||||
self.semvar = semvar
|
||||
public var semvar: SemVar? {
|
||||
guard case let .semvar(preRelease, requireExistingFile, requireExistingSemVar) = self
|
||||
else { return nil }
|
||||
return .init(
|
||||
preRelease: preRelease,
|
||||
requireExistingFile: requireExistingFile ?? false,
|
||||
requireExistingSemVar: requireExistingSemVar ?? false
|
||||
)
|
||||
}
|
||||
|
||||
public static func branch(_ branch: Branch) -> Self {
|
||||
.branch(includeCommitSha: branch.includeCommitSha)
|
||||
}
|
||||
|
||||
public static func semvar(_ value: SemVar) -> Self {
|
||||
.semvar(
|
||||
preRelease: value.preRelease,
|
||||
requireExistingFile: value.requireExistingFile,
|
||||
requireExistingSemVar: value.requireExistingSemVar
|
||||
)
|
||||
}
|
||||
|
||||
public var customDumpMirror: Mirror {
|
||||
switch self {
|
||||
case .branch:
|
||||
return .init(self, children: ["branch": branch!], displayStyle: .struct)
|
||||
case .semvar:
|
||||
return .init(self, children: ["semvar": semvar!], displayStyle: .struct)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,20 +17,20 @@ public extension DependencyValues {
|
||||
public struct ConfigurationClient: Sendable {
|
||||
|
||||
/// Find a configuration file in the given directory or in current working directory.
|
||||
public var find: @Sendable (URL?) async throws -> ConfigruationFile?
|
||||
public var find: @Sendable (URL?) async throws -> URL?
|
||||
|
||||
/// Load a configuration file.
|
||||
public var load: @Sendable (ConfigruationFile) async throws -> Configuration
|
||||
public var load: @Sendable (URL) async throws -> Configuration
|
||||
|
||||
/// Write a configuration file.
|
||||
public var write: @Sendable (Configuration, ConfigruationFile) async throws -> Void
|
||||
public var write: @Sendable (Configuration, URL) async throws -> Void
|
||||
|
||||
/// Find a configuration file and load it if found.
|
||||
public func findAndLoad(_ url: URL? = nil) async throws -> Configuration {
|
||||
guard let url = try await find(url) else {
|
||||
guard let url = try? await find(url) else {
|
||||
throw ConfigurationClientError.configurationNotFound
|
||||
}
|
||||
return try await load(url)
|
||||
return (try? await load(url)) ?? .default
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,43 +40,48 @@ extension ConfigurationClient: DependencyKey {
|
||||
public static var liveValue: ConfigurationClient {
|
||||
.init(
|
||||
find: { try await findConfiguration($0) },
|
||||
load: { try await $0.load() ?? .mock },
|
||||
write: { try await $1.write($0) }
|
||||
load: { try await loadConfiguration($0) },
|
||||
write: { try await writeConfiguration($0, to: $1) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func findConfiguration(_ url: URL?) async throws -> ConfigruationFile? {
|
||||
private func findConfiguration(_ url: URL?) async throws -> URL? {
|
||||
@Dependency(\.fileClient) var fileClient
|
||||
|
||||
let defaultFileName = ConfigurationClient.Constants.defaultFileNameWithoutExtension
|
||||
|
||||
var url: URL! = url
|
||||
if url == nil {
|
||||
url = try await URL(filePath: fileClient.currentDirectory())
|
||||
}
|
||||
|
||||
// Check if url is a valid configuration url.
|
||||
var configurationFile = ConfigruationFile(url: url)
|
||||
if let configurationFile { return configurationFile }
|
||||
|
||||
guard try await fileClient.isDirectory(url.cleanFilePath) else {
|
||||
throw ConfigurationClientError.invalidConfigurationDirectory(path: url.cleanFilePath)
|
||||
if try await fileClient.isDirectory(url.cleanFilePath) {
|
||||
url = url.appending(path: "\(defaultFileName).json")
|
||||
}
|
||||
|
||||
// Check for toml file.
|
||||
let tomlUrl = url.appending(path: "\(ConfigurationClient.Constants.defaultFileNameWithoutExtension).toml")
|
||||
configurationFile = ConfigruationFile(url: tomlUrl)
|
||||
if let configurationFile { return configurationFile }
|
||||
|
||||
// Check for json file.
|
||||
let jsonUrl = url.appending(path: "\(ConfigurationClient.Constants.defaultFileNameWithoutExtension).json")
|
||||
configurationFile = ConfigruationFile(url: jsonUrl)
|
||||
if let configurationFile { return configurationFile }
|
||||
|
||||
// Couldn't find valid configuration file.
|
||||
if fileClient.fileExists(url) {
|
||||
return url
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func loadConfiguration(_ url: URL) async throws -> Configuration {
|
||||
@Dependency(\.coders.jsonDecoder) var jsonDecoder
|
||||
@Dependency(\.fileClient) var fileClient
|
||||
|
||||
let string = try await fileClient.read(url.cleanFilePath)
|
||||
return try jsonDecoder().decode(Configuration.self, from: Data(string.utf8))
|
||||
}
|
||||
|
||||
enum ConfigurationClientError: Error {
|
||||
case configurationNotFound
|
||||
case invalidConfigurationDirectory(path: String)
|
||||
}
|
||||
|
||||
private func writeConfiguration(_ configuration: Configuration, to url: URL) async throws {
|
||||
@Dependency(\.fileClient) var fileClient
|
||||
@Dependency(\.coders.jsonEncoder) var jsonEncoder
|
||||
let data = try jsonEncoder().encode(configuration)
|
||||
try await fileClient.write(data, url)
|
||||
}
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import Dependencies
|
||||
import FileClient
|
||||
import Foundation
|
||||
|
||||
/// Represents a configuration file type and location.
|
||||
public enum ConfigruationFile: Equatable, Sendable {
|
||||
|
||||
/// A json configuration file.
|
||||
case json(URL)
|
||||
|
||||
/// A toml configuration file.
|
||||
case toml(URL)
|
||||
|
||||
/// Default configuration file, which is a toml file
|
||||
/// with the name of '.bump-version.toml'
|
||||
public static var `default`: Self {
|
||||
.toml(URL(
|
||||
filePath: "\(ConfigurationClient.Constants.defaultFileNameWithoutExtension).toml"
|
||||
))
|
||||
}
|
||||
|
||||
/// Create a new file location from the given url.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - url: The url for the file.
|
||||
public init?(url: URL) {
|
||||
if url.pathExtension == "toml" {
|
||||
self = .toml(url)
|
||||
} else if url.pathExtension == "json" {
|
||||
self = .json(url)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// The url of the file.
|
||||
var url: URL {
|
||||
switch self {
|
||||
case let .json(url): return url
|
||||
case let .toml(url): return url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ConfigruationFile {
|
||||
|
||||
func load() async throws -> Configuration? {
|
||||
@Dependency(\.coders) var coders
|
||||
@Dependency(\.fileClient) var fileClient
|
||||
|
||||
switch self {
|
||||
case .json:
|
||||
let data = try await Data(fileClient.read(url.cleanFilePath).utf8)
|
||||
return try? coders.jsonDecoder().decode(Configuration.self, from: data)
|
||||
case .toml:
|
||||
let string = try await fileClient.read(url.cleanFilePath)
|
||||
return try? coders.tomlDecoder().decode(Configuration.self, from: string)
|
||||
}
|
||||
}
|
||||
|
||||
func write(_ configuration: Configuration) async throws {
|
||||
@Dependency(\.coders) var coders
|
||||
@Dependency(\.fileClient) var fileClient
|
||||
|
||||
let data: Data
|
||||
|
||||
switch self {
|
||||
case .json:
|
||||
data = try coders.jsonEncoder().encode(configuration)
|
||||
case .toml:
|
||||
data = try Data(coders.tomlEncoder().encode(configuration).utf8)
|
||||
}
|
||||
|
||||
try await fileClient.write(data, url)
|
||||
}
|
||||
}
|
||||
17
Sources/cli-version/Application.swift
Normal file
17
Sources/cli-version/Application.swift
Normal file
@@ -0,0 +1,17 @@
|
||||
import ArgumentParser
|
||||
import Foundation
|
||||
|
||||
@main
|
||||
struct Application: AsyncParsableCommand {
|
||||
static let configuration: CommandConfiguration = .init(
|
||||
commandName: "bump-version",
|
||||
version: VERSION ?? "0.0.0",
|
||||
subcommands: [
|
||||
BuildCommand.self,
|
||||
BumpCommand.self,
|
||||
GenerateCommand.self,
|
||||
UtilsCommand.self
|
||||
],
|
||||
defaultSubcommand: BumpCommand.self
|
||||
)
|
||||
}
|
||||
@@ -1,47 +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.",
|
||||
subcommands: [BranchStyle.self, SemVarStyle.self],
|
||||
defaultSubcommand: SemVarStyle.self
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension CliVersionCommand.Build {
|
||||
struct BranchStyle: AsyncParsableCommand {
|
||||
|
||||
static let configuration: CommandConfiguration = .init(
|
||||
commandName: "branch",
|
||||
abstract: "Build using branch and commit sha as the version.",
|
||||
discussion: "This should generally not be interacted with directly, outside of the plugin usage context."
|
||||
)
|
||||
|
||||
@OptionGroup var globals: GlobalBranchOptions
|
||||
|
||||
func run() async throws {
|
||||
try await globals.shared().run(\.build)
|
||||
}
|
||||
}
|
||||
|
||||
struct SemVarStyle: AsyncParsableCommand {
|
||||
|
||||
static let configuration: CommandConfiguration = .init(
|
||||
commandName: "semvar",
|
||||
abstract: "Generates a version file with SemVar style.",
|
||||
discussion: "This should generally not be interacted with directly, outside of the plugin usage context."
|
||||
)
|
||||
|
||||
@OptionGroup var globals: GlobalSemVarOptions
|
||||
|
||||
func run() async throws {
|
||||
try await globals.shared().run(\.build)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +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.",
|
||||
subcommands: [
|
||||
SemVarStyle.self,
|
||||
BranchStyle.self
|
||||
],
|
||||
defaultSubcommand: SemVarStyle.self
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension CliVersionCommand.Bump {
|
||||
|
||||
struct BranchStyle: AsyncParsableCommand {
|
||||
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: "branch",
|
||||
abstract: "Bump using the current branch and commit sha."
|
||||
)
|
||||
|
||||
@OptionGroup var globals: GlobalBranchOptions
|
||||
|
||||
func run() async throws {
|
||||
try await globals.shared().run(\.bump, args: nil)
|
||||
}
|
||||
}
|
||||
|
||||
struct SemVarStyle: AsyncParsableCommand {
|
||||
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: "semvar",
|
||||
abstract: "Bump using semvar style options."
|
||||
)
|
||||
|
||||
@OptionGroup var globals: GlobalSemVarOptions
|
||||
|
||||
@Flag
|
||||
var bumpOption: CliClient.BumpOption = .patch
|
||||
|
||||
func run() async throws {
|
||||
try await globals.shared().run(\.bump, args: bumpOption)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
extension CliClient.BumpOption: EnumerableFlag {}
|
||||
@@ -1,16 +0,0 @@
|
||||
import ArgumentParser
|
||||
import Foundation
|
||||
|
||||
@main
|
||||
struct CliVersionCommand: AsyncParsableCommand {
|
||||
static let configuration: CommandConfiguration = .init(
|
||||
commandName: "cli-version",
|
||||
version: VERSION ?? "0.0.0",
|
||||
subcommands: [
|
||||
Build.self,
|
||||
Bump.self,
|
||||
Generate.self
|
||||
],
|
||||
defaultSubcommand: Bump.self
|
||||
)
|
||||
}
|
||||
19
Sources/cli-version/Commands/BuildCommand.swift
Normal file
19
Sources/cli-version/Commands/BuildCommand.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
26
Sources/cli-version/Commands/BumpCommand.swift
Normal file
26
Sources/cli-version/Commands/BumpCommand.swift
Normal 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 {}
|
||||
19
Sources/cli-version/Commands/GenerateCommand.swift
Normal file
19
Sources/cli-version/Commands/GenerateCommand.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
95
Sources/cli-version/Commands/UtilsCommand.swift
Normal file
95
Sources/cli-version/Commands/UtilsCommand.swift
Normal 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
|
||||
}
|
||||
}
|
||||
5
Sources/cli-version/Documentation.docc/Application.md
Normal file
5
Sources/cli-version/Documentation.docc/Application.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Application
|
||||
|
||||
## Articles
|
||||
|
||||
- <doc:Installation>
|
||||
@@ -0,0 +1,3 @@
|
||||
# Installation
|
||||
|
||||
You can install the command-line application by...
|
||||
@@ -1,52 +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.",
|
||||
subcommands: [BranchStyle.self, SemVarStyle.self],
|
||||
defaultSubcommand: SemVarStyle.self
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension CliVersionCommand.Generate {
|
||||
struct BranchStyle: AsyncParsableCommand {
|
||||
|
||||
static let configuration: CommandConfiguration = .init(
|
||||
commandName: "branch",
|
||||
abstract: "Generates a version file with branch and commit sha as the version.",
|
||||
discussion: "This command can be interacted with directly, outside of the plugin usage context."
|
||||
)
|
||||
|
||||
@OptionGroup var globals: GlobalBranchOptions
|
||||
|
||||
func run() async throws {
|
||||
try await globals.shared().run(\.generate)
|
||||
}
|
||||
}
|
||||
|
||||
struct SemVarStyle: AsyncParsableCommand {
|
||||
|
||||
static let configuration: CommandConfiguration = .init(
|
||||
commandName: "semvar",
|
||||
abstract: "Generates a version file with SemVar style.",
|
||||
discussion: "This command can be interacted with directly, outside of the plugin usage context."
|
||||
)
|
||||
|
||||
@OptionGroup var globals: GlobalSemVarOptions
|
||||
|
||||
func run() async throws {
|
||||
try await globals.shared().run(\.generate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum GenerationError: Error {
|
||||
case fileExists(path: String)
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
import ArgumentParser
|
||||
@_spi(Internal) import CliClient
|
||||
import ConfigurationClient
|
||||
import Dependencies
|
||||
import Foundation
|
||||
import Rainbow
|
||||
|
||||
struct GlobalOptions<Child: ParsableArguments>: ParsableArguments {
|
||||
struct GlobalOptions: ParsableArguments {
|
||||
|
||||
@OptionGroup var targetOptions: TargetOptions
|
||||
@OptionGroup var child: Child
|
||||
@OptionGroup
|
||||
var configOptions: ConfigurationOptions
|
||||
|
||||
@Option(
|
||||
name: .customLong("git-directory"),
|
||||
@@ -27,9 +28,38 @@ struct GlobalOptions<Child: ParsableArguments>: 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 Empty: ParsableArguments {}
|
||||
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 {
|
||||
@Option(
|
||||
@@ -53,6 +83,13 @@ struct TargetOptions: ParsableArguments {
|
||||
}
|
||||
|
||||
struct PreReleaseOptions: ParsableArguments {
|
||||
|
||||
@Flag(
|
||||
name: .shortAndLong,
|
||||
help: ""
|
||||
)
|
||||
var disablePreRelease: Bool = false
|
||||
|
||||
@Flag(
|
||||
name: [.customShort("s"), .customLong("pre-release-branch-style")],
|
||||
help: """
|
||||
@@ -70,13 +107,21 @@ struct PreReleaseOptions: ParsableArguments {
|
||||
var useTagAsPreRelease: Bool = false
|
||||
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
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\")
|
||||
Add / use a pre-release prefix string.
|
||||
"""
|
||||
)
|
||||
var custom: String?
|
||||
var preReleasePrefix: String?
|
||||
|
||||
@Flag(
|
||||
name: .long,
|
||||
help: """
|
||||
Apply custom pre-release suffix, using extra options / arguments passed in after a '--'.
|
||||
"""
|
||||
)
|
||||
var customPreRelease: Bool = false
|
||||
|
||||
}
|
||||
|
||||
struct SemVarOptions: ParsableArguments {
|
||||
@@ -97,114 +142,3 @@ struct SemVarOptions: ParsableArguments {
|
||||
|
||||
@OptionGroup var preRelease: PreReleaseOptions
|
||||
}
|
||||
|
||||
typealias GlobalSemVarOptions = GlobalOptions<SemVarOptions>
|
||||
typealias GlobalBranchOptions = GlobalOptions<Empty>
|
||||
|
||||
extension GlobalSemVarOptions {
|
||||
func shared() throws -> CliClient.SharedOptions {
|
||||
try shared(.semVar(child.semVarOptions()))
|
||||
}
|
||||
}
|
||||
|
||||
extension GlobalBranchOptions {
|
||||
func shared() throws -> CliClient.SharedOptions {
|
||||
try shared(.branchAndCommit)
|
||||
}
|
||||
}
|
||||
|
||||
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(_ versionStrategy: CliClient.VersionStrategy) throws -> CliClient.SharedOptions {
|
||||
try .init(
|
||||
dryRun: dryRun,
|
||||
gitDirectory: gitDirectory,
|
||||
logLevel: .init(verbose: verbose),
|
||||
target: targetOptions.target(),
|
||||
versionStrategy: versionStrategy
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension TargetOptions {
|
||||
func target() throws -> String {
|
||||
guard let path else {
|
||||
guard let module else {
|
||||
print("Neither target path or module was set.")
|
||||
throw InvalidTargetOption()
|
||||
}
|
||||
|
||||
return "\(module)/\(fileName)"
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
struct InvalidTargetOption: Error {}
|
||||
}
|
||||
|
||||
extension PreReleaseOptions {
|
||||
|
||||
func preReleaseStrategy() throws -> CliClient.PreReleaseStrategy? {
|
||||
guard let custom else {
|
||||
if useBranchAsPreRelease {
|
||||
return .branchAndCommit
|
||||
} else if useTagAsPreRelease {
|
||||
return .tag
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if useBranchAsPreRelease {
|
||||
return .custom(custom, .branchAndCommit)
|
||||
} else if useTagAsPreRelease {
|
||||
return .custom(custom, .tag)
|
||||
} else {
|
||||
return .custom(custom, nil)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SemVarOptions {
|
||||
func semVarOptions() throws -> CliClient.VersionStrategy.SemVarOptions {
|
||||
try .init(
|
||||
preReleaseStrategy: preRelease.preReleaseStrategy(),
|
||||
requireExistingFile: requireExistingFile,
|
||||
requireExistingSemVar: requireExistingSemvar
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
123
Sources/cli-version/Helpers/GlobalOptions+run.swift
Normal file
123
Sources/cli-version/Helpers/GlobalOptions+run.swift
Normal 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 {}
|
||||
@@ -1,4 +1,5 @@
|
||||
@_spi(Internal) import CliClient
|
||||
import ConfigurationClient
|
||||
import Dependencies
|
||||
import FileClient
|
||||
import Foundation
|
||||
@@ -20,7 +21,7 @@ struct CliClientTests {
|
||||
@Dependency(\.cliClient) var client
|
||||
let output = try await client.build(.testOptions(
|
||||
target: target,
|
||||
versionStrategy: .semVar(.init(requireExistingFile: false))
|
||||
versionStrategy: .semvar(requireExistingFile: false)
|
||||
))
|
||||
#expect(output == "/baz/Sources/bar/Version.swift")
|
||||
}
|
||||
@@ -65,7 +66,7 @@ struct CliClientTests {
|
||||
@Dependency(\.cliClient) var client
|
||||
let output = try await client.build(.testOptions(
|
||||
target: target,
|
||||
versionStrategy: .semVar(.init(requireExistingFile: false))
|
||||
versionStrategy: .semvar(requireExistingFile: false)
|
||||
))
|
||||
#expect(output == "/baz/Sources/bar/Version.swift")
|
||||
}
|
||||
@@ -96,6 +97,8 @@ struct CliClientTests {
|
||||
$0.fileClient.fileExists = { _ in false }
|
||||
$0.gitClient = .mock(.tag("1.0.0"))
|
||||
$0.cliClient = .liveValue
|
||||
$0.configurationClient = .liveValue
|
||||
$0.configurationClient.find = { _ in URL(filePath: "/") }
|
||||
setupDependencies(&$0)
|
||||
} operation: {
|
||||
try await operation()
|
||||
@@ -113,7 +116,7 @@ struct CliClientTests {
|
||||
}
|
||||
|
||||
enum TestArguments {
|
||||
static let testCases = ["bar", "Sources/bar", "/Sources/bar", "./Sources/bar"]
|
||||
static let testCases = ["bar", "Sources/bar", "./Sources/bar"]
|
||||
static let bumpCases = CliClient.BumpOption.allCases.reduce(into: [(CliClient.BumpOption, Bool)]()) {
|
||||
$0.append(($1, true))
|
||||
$0.append(($1, false))
|
||||
@@ -130,14 +133,15 @@ extension CliClient.SharedOptions {
|
||||
dryRun: Bool = false,
|
||||
target: String = "bar",
|
||||
logLevel: Logger.Level = .trace,
|
||||
versionStrategy: CliClient.VersionStrategy = .semVar(.init())
|
||||
versionStrategy: Configuration.VersionStrategy = .semvar(.init())
|
||||
) -> Self {
|
||||
.init(
|
||||
return .init(
|
||||
dryRun: dryRun,
|
||||
gitDirectory: gitDirectory,
|
||||
logLevel: logLevel,
|
||||
target: target,
|
||||
versionStrategy: versionStrategy
|
||||
target: .init(module: .init(target)),
|
||||
branch: versionStrategy.branch,
|
||||
semvar: versionStrategy.semvar
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,16 +29,16 @@ final class GitVersionTests: XCTestCase {
|
||||
.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
|
||||
// #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
|
||||
|
||||
@@ -2,6 +2,7 @@ import ConfigurationClient
|
||||
import Dependencies
|
||||
import Foundation
|
||||
import Testing
|
||||
import TestSupport
|
||||
|
||||
@Suite("ConfigurationClientTests")
|
||||
struct ConfigurationClientTests {
|
||||
@@ -17,27 +18,58 @@ struct ConfigurationClientTests {
|
||||
let decoded = try coders.jsonDecoder().decode(Configuration.self, from: encoded)
|
||||
|
||||
#expect(decoded == configuration)
|
||||
|
||||
let tomlEncoded = try coders.tomlEncoder().encode(configuration)
|
||||
let tomlDecoded = try coders.tomlDecoder().decode(
|
||||
Configuration.self,
|
||||
from: tomlEncoded
|
||||
)
|
||||
#expect(tomlDecoded == configuration)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(arguments: ["foo", ".foo"])
|
||||
func configurationFile(fileName: String) {
|
||||
for ext in ["toml", "json", "bar"] {
|
||||
let file = ConfigruationFile(url: URL(filePath: "\(fileName).\(ext)"))
|
||||
switch ext {
|
||||
case "toml":
|
||||
#expect(file == .toml(URL(filePath: "\(fileName).toml")))
|
||||
case "json":
|
||||
#expect(file == .json(URL(filePath: "\(fileName).json")))
|
||||
default:
|
||||
#expect(file == nil)
|
||||
@Test
|
||||
func writeAndLoad() async throws {
|
||||
try await withTemporaryDirectory { url in
|
||||
try await run {
|
||||
@Dependency(\.configurationClient) var configurationClient
|
||||
|
||||
for ext in ["json"] {
|
||||
let fileUrl = url.appending(path: "test.\(ext)")
|
||||
let configuration = Configuration.mock()
|
||||
|
||||
try await configurationClient.write(configuration, fileUrl)
|
||||
let loaded = try await configurationClient.load(fileUrl)
|
||||
#expect(loaded == configuration)
|
||||
|
||||
let findAndLoaded = try await configurationClient.findAndLoad(fileUrl)
|
||||
#expect(findAndLoaded == configuration)
|
||||
|
||||
try FileManager.default.removeItem(at: fileUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func findAndLoad() async throws {
|
||||
try await withTemporaryDirectory { url in
|
||||
try await run {
|
||||
@Dependency(\.configurationClient) var configurationClient
|
||||
|
||||
let shouldBeNil = try await configurationClient.find(url)
|
||||
#expect(shouldBeNil == nil)
|
||||
|
||||
do {
|
||||
_ = try await configurationClient.findAndLoad(url)
|
||||
#expect(Bool(false))
|
||||
} catch {
|
||||
#expect(Bool(true))
|
||||
}
|
||||
|
||||
for ext in ["json"] {
|
||||
let fileUrl = url.appending(path: ".bump-version.\(ext)")
|
||||
let configuration = Configuration.mock()
|
||||
|
||||
try await configurationClient.write(configuration, fileUrl)
|
||||
let loaded = try await configurationClient.findAndLoad(fileUrl)
|
||||
#expect(loaded == configuration)
|
||||
|
||||
try FileManager.default.removeItem(at: fileUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
59
Tests/GitClientTests/GitClientTests.swift
Normal file
59
Tests/GitClientTests/GitClientTests.swift
Normal file
@@ -0,0 +1,59 @@
|
||||
import Dependencies
|
||||
import GitClient
|
||||
import ShellClient
|
||||
import Testing
|
||||
|
||||
@Suite("GitClientTests")
|
||||
struct GitClientTests {
|
||||
|
||||
@Test(arguments: GitClientVersionTestArgument.testCases)
|
||||
func testGitClient(input: GitClientVersionTestArgument) async throws {
|
||||
let arguments = try await run {
|
||||
@Dependency(\.gitClient) var gitClient
|
||||
_ = try await gitClient.version(.init(style: input.style))
|
||||
}
|
||||
#expect(arguments == input.expected)
|
||||
}
|
||||
|
||||
func run(
|
||||
_ operation: () async throws -> Void
|
||||
) async throws -> [[String]] {
|
||||
let captured = CapturedCommand()
|
||||
|
||||
try await withDependencies {
|
||||
$0.asyncShellClient = .capturing(captured)
|
||||
$0.fileClient = .noop
|
||||
$0.gitClient = .liveValue
|
||||
} operation: {
|
||||
try await operation()
|
||||
}
|
||||
return await captured.commands.map(\.arguments)
|
||||
}
|
||||
}
|
||||
|
||||
struct GitClientVersionTestArgument {
|
||||
let style: GitClient.CurrentVersionOption.Style
|
||||
let expected: [[String]]
|
||||
|
||||
static let testCases: [Self] = [
|
||||
.init(
|
||||
style: .tag(exactMatch: true),
|
||||
expected: [["git", "describe", "--tags", "--exact-match"]]
|
||||
),
|
||||
.init(
|
||||
style: .tag(exactMatch: false),
|
||||
expected: [["git", "describe", "--tags"]]
|
||||
),
|
||||
.init(
|
||||
style: .branch(commitSha: false),
|
||||
expected: [["git", "symbolic-ref", "--quiet", "--short", "HEAD"]]
|
||||
),
|
||||
.init(
|
||||
style: .branch(commitSha: true),
|
||||
expected: [
|
||||
["git", "rev-parse", "--short", "HEAD"],
|
||||
["git", "symbolic-ref", "--quiet", "--short", "HEAD"]
|
||||
]
|
||||
)
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user