feat: Working on bump version.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"originHash" : "07a243cd8c2ed649f5fe5e76202ed091834801a0637c8b7785404e20b9e0cac1",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "combine-schedulers",
|
||||
@@ -95,8 +96,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/m-housh/swift-shell-client.git",
|
||||
"state" : {
|
||||
"revision" : "9c0c4757d0a2e313d1a4a28e60418a753d3e4230",
|
||||
"version" : "0.1.4"
|
||||
"revision" : "ea819f41b87aa94e792f028a031f5db786e79a94",
|
||||
"version" : "0.2.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -118,5 +119,5 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
"version" : 3
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import PackageDescription
|
||||
let package = Package(
|
||||
name: "swift-cli-version",
|
||||
platforms: [
|
||||
.macOS(.v10_15)
|
||||
.macOS(.v13)
|
||||
],
|
||||
products: [
|
||||
.library(name: "CliVersion", targets: ["CliVersion"]),
|
||||
@@ -15,7 +15,7 @@ let package = Package(
|
||||
],
|
||||
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.1.3"),
|
||||
.package(url: "https://github.com/m-housh/swift-shell-client.git", from: "0.2.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.2.2")
|
||||
],
|
||||
|
||||
@@ -16,16 +16,46 @@ public extension DependencyValues {
|
||||
|
||||
/// Handles the command-line commands.
|
||||
@DependencyClient
|
||||
public struct CliClient {
|
||||
public struct CliClient: Sendable {
|
||||
|
||||
/// Build and update the version based on the git tag, or branch + sha.
|
||||
public var build: @Sendable (BuildOptions) throws -> String
|
||||
public var build: @Sendable (SharedOptions) async throws -> String
|
||||
|
||||
public var bump: @Sendable (BumpOption, SharedOptions) async throws -> String
|
||||
|
||||
/// Generate a version file with an optional version that can be set manually.
|
||||
public var generate: @Sendable (GenerateOptions) throws -> String
|
||||
public var generate: @Sendable (SharedOptions) async throws -> String
|
||||
|
||||
/// Update a version file manually.
|
||||
public var update: @Sendable (UpdateOptions) throws -> String
|
||||
public var update: @Sendable (SharedOptions) async throws -> String
|
||||
|
||||
public enum BumpOption: Sendable, CaseIterable {
|
||||
case major, minor, patch
|
||||
}
|
||||
|
||||
// TODO: Use Int for `verbose`.
|
||||
public struct SharedOptions: Equatable, Sendable {
|
||||
let gitDirectory: String?
|
||||
let dryRun: Bool
|
||||
let fileName: String
|
||||
let target: String
|
||||
let verbose: Bool
|
||||
|
||||
public init(
|
||||
gitDirectory: String? = nil,
|
||||
dryRun: Bool = false,
|
||||
fileName: String = "Version.swift",
|
||||
target: String,
|
||||
verbose: Bool = true
|
||||
) {
|
||||
self.gitDirectory = gitDirectory
|
||||
self.dryRun = dryRun
|
||||
self.fileName = fileName
|
||||
self.target = target
|
||||
self.verbose = verbose
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension CliClient: DependencyKey {
|
||||
@@ -33,9 +63,10 @@ extension CliClient: DependencyKey {
|
||||
|
||||
public static func live(environment: [String: String]) -> Self {
|
||||
.init(
|
||||
build: { try $0.run(environment) },
|
||||
generate: { try $0.run() },
|
||||
update: { try $0.run() }
|
||||
build: { try $0.build(environment) },
|
||||
bump: { try $1.bump($0) },
|
||||
generate: { try $0.generate() },
|
||||
update: { try $0.update() }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -44,82 +75,24 @@ extension CliClient: DependencyKey {
|
||||
}
|
||||
}
|
||||
|
||||
public extension CliClient {
|
||||
|
||||
// TODO: Use Int for `verbose`.
|
||||
struct SharedOptions: Sendable {
|
||||
let dryRun: Bool
|
||||
let fileName: String
|
||||
let target: String
|
||||
let verbose: Bool
|
||||
|
||||
public init(
|
||||
dryRun: Bool = false,
|
||||
fileName: String,
|
||||
target: String,
|
||||
verbose: Bool = true
|
||||
) {
|
||||
self.dryRun = dryRun
|
||||
self.fileName = fileName
|
||||
self.target = target
|
||||
self.verbose = verbose
|
||||
}
|
||||
}
|
||||
|
||||
struct BuildOptions: Sendable {
|
||||
let gitDirectory: String?
|
||||
let shared: SharedOptions
|
||||
|
||||
public init(
|
||||
gitDirectory: String? = nil,
|
||||
shared: SharedOptions
|
||||
) {
|
||||
self.gitDirectory = gitDirectory
|
||||
self.shared = shared
|
||||
}
|
||||
}
|
||||
|
||||
struct GenerateOptions: Sendable {
|
||||
let shared: SharedOptions
|
||||
|
||||
public init(shared: SharedOptions) {
|
||||
self.shared = shared
|
||||
}
|
||||
}
|
||||
|
||||
struct UpdateOptions: Sendable {
|
||||
let gitDirectory: String?
|
||||
let shared: SharedOptions
|
||||
|
||||
public init(
|
||||
gitDirectory: String? = nil,
|
||||
shared: SharedOptions
|
||||
) {
|
||||
self.gitDirectory = gitDirectory
|
||||
self.shared = shared
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@_spi(Internal)
|
||||
public extension CliClient.SharedOptions {
|
||||
|
||||
var fileUrl: URL {
|
||||
url(for: target).appendingPathComponent(fileName)
|
||||
}
|
||||
let target = self.target.hasPrefix(".") ? String(self.target.dropFirst()) : self.target
|
||||
let targetHasSources = target.hasPrefix("Sources") || target.hasPrefix("/Sources")
|
||||
|
||||
func parseTarget() throws -> URL {
|
||||
let targetUrl = fileUrl
|
||||
.deletingLastPathComponent()
|
||||
.deletingLastPathComponent()
|
||||
|
||||
guard targetUrl.lastPathComponent == "Sources" else {
|
||||
return url(for: "Sources")
|
||||
.appendingPathComponent(target)
|
||||
.appendingPathComponent(fileName)
|
||||
var url = url(for: gitDirectory ?? (targetHasSources ? target : "Sources"))
|
||||
if gitDirectory != nil {
|
||||
if !targetHasSources {
|
||||
url.appendPathComponent("Sources")
|
||||
}
|
||||
return fileUrl
|
||||
url.appendPathComponent(target)
|
||||
}
|
||||
url.appendPathComponent(fileName)
|
||||
return url
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
@@ -134,10 +107,10 @@ public extension CliClient.SharedOptions {
|
||||
}
|
||||
}
|
||||
|
||||
private extension CliClient.BuildOptions {
|
||||
private extension CliClient.SharedOptions {
|
||||
|
||||
func run(_ environment: [String: String]) throws -> String {
|
||||
try shared.run {
|
||||
func build(_ environment: [String: String]) throws -> String {
|
||||
try run {
|
||||
@Dependency(\.gitVersionClient) var gitVersion
|
||||
@Dependency(\.fileClient) var fileClient
|
||||
@Dependency(\.logger) var logger
|
||||
@@ -150,28 +123,74 @@ private extension CliClient.BuildOptions {
|
||||
|
||||
logger.debug("Building with git directory: \(gitDirectory)")
|
||||
|
||||
let fileUrl = shared.fileUrl
|
||||
let fileUrl = self.fileUrl
|
||||
logger.debug("File url: \(fileUrl.cleanFilePath)")
|
||||
|
||||
let currentVersion = try gitVersion.currentVersion(in: gitDirectory)
|
||||
|
||||
let fileContents = buildTemplate
|
||||
.replacingOccurrences(of: "nil", with: "\"\(currentVersion)\"")
|
||||
let fileContents = Template.build(currentVersion)
|
||||
|
||||
try fileClient.write(string: fileContents, to: fileUrl)
|
||||
|
||||
return fileUrl.cleanFilePath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension CliClient.GenerateOptions {
|
||||
|
||||
func run() throws -> String {
|
||||
func bump(_ type: CliClient.BumpOption) throws -> String {
|
||||
@Dependency(\.fileClient) var fileClient
|
||||
@Dependency(\.logger) var logger
|
||||
|
||||
let targetUrl = try shared.parseTarget()
|
||||
let targetUrl = fileUrl
|
||||
|
||||
logger.debug("Bump target url: \(targetUrl.cleanFilePath)")
|
||||
|
||||
let contents = try fileClient.read(fileUrl.cleanFilePath)
|
||||
let versionLine = contents.split(separator: "\n")
|
||||
.first { $0.hasPrefix("let VERSION:") }
|
||||
|
||||
guard let versionLine else {
|
||||
throw CliClientError.failedToParseVersionFile
|
||||
}
|
||||
|
||||
let isOptional = versionLine.contains("String?")
|
||||
let versionString = versionLine.split(separator: "let VERSION: \(isOptional ? "String?" : "String") = ").last
|
||||
guard let versionString else {
|
||||
throw CliClientError.failedToParseVersionFile
|
||||
}
|
||||
|
||||
let parts = String(versionString).split(separator: ".")
|
||||
logger.debug("Version parts: \(parts)")
|
||||
|
||||
// TODO: Better error.
|
||||
guard parts.count == 3 else {
|
||||
throw CliClientError.failedToParseVersionFile
|
||||
}
|
||||
|
||||
var major = Int(String(parts[0])) ?? 0
|
||||
var minor = Int(String(parts[1])) ?? 0
|
||||
var patch = Int(String(parts[2])) ?? 0
|
||||
|
||||
type.bump(major: &major, minor: &minor, patch: &patch)
|
||||
|
||||
let version = "\(major).\(minor).\(patch)"
|
||||
logger.debug("Bumped version: \(version)")
|
||||
|
||||
let template = isOptional ? Template.optional(version) : Template.build(version)
|
||||
|
||||
if !dryRun {
|
||||
try fileClient.write(string: template, to: targetUrl)
|
||||
} else {
|
||||
logger.debug("Skipping, due to dry-run being passed.")
|
||||
}
|
||||
|
||||
return targetUrl.cleanFilePath
|
||||
}
|
||||
|
||||
func generate(_ version: String? = nil) throws -> String {
|
||||
@Dependency(\.fileClient) var fileClient
|
||||
@Dependency(\.logger) var logger
|
||||
|
||||
let targetUrl = fileUrl
|
||||
|
||||
logger.debug("Generate target url: \(targetUrl.cleanFilePath)")
|
||||
|
||||
@@ -179,37 +198,68 @@ private extension CliClient.GenerateOptions {
|
||||
throw CliClientError.fileExists(path: targetUrl.cleanFilePath)
|
||||
}
|
||||
|
||||
if !shared.dryRun {
|
||||
try fileClient.write(string: optionalTemplate, to: targetUrl)
|
||||
let template = Template.optional(version)
|
||||
|
||||
if !dryRun {
|
||||
try fileClient.write(string: template, to: targetUrl)
|
||||
} else {
|
||||
logger.debug("Skipping, due to dry-run being passed.")
|
||||
}
|
||||
return targetUrl.cleanFilePath
|
||||
}
|
||||
|
||||
func update() throws -> String {
|
||||
@Dependency(\.gitVersionClient) var gitVersionClient
|
||||
return try generate(gitVersionClient.currentVersion(in: gitDirectory))
|
||||
}
|
||||
}
|
||||
|
||||
private extension CliClient.UpdateOptions {
|
||||
@_spi(Internal)
|
||||
public extension CliClient.BumpOption {
|
||||
|
||||
func run() throws -> String {
|
||||
@Dependency(\.fileClient) var fileClient
|
||||
@Dependency(\.gitVersionClient) var gitVersionClient
|
||||
@Dependency(\.logger) var logger
|
||||
|
||||
let targetUrl = try shared.parseTarget()
|
||||
logger.debug("Target url: \(targetUrl.cleanFilePath)")
|
||||
|
||||
let currentVersion = try gitVersionClient.currentVersion(in: gitDirectory)
|
||||
|
||||
let fileContents = optionalTemplate
|
||||
.replacingOccurrences(of: "nil", with: "\"\(currentVersion)\"")
|
||||
|
||||
if !shared.dryRun {
|
||||
try fileClient.write(string: fileContents, to: targetUrl)
|
||||
} else {
|
||||
logger.debug("Skipping due to dry run being passed.")
|
||||
logger.debug("Parsed version: \(currentVersion)")
|
||||
func bump(
|
||||
major: inout Int,
|
||||
minor: inout Int,
|
||||
patch: inout Int
|
||||
) {
|
||||
switch self {
|
||||
case .major:
|
||||
major += 1
|
||||
minor = 0
|
||||
patch = 0
|
||||
case .minor:
|
||||
minor += 1
|
||||
patch = 0
|
||||
case .patch:
|
||||
patch += 1
|
||||
}
|
||||
return targetUrl.cleanFilePath
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(Internal)
|
||||
public struct Template {
|
||||
let type: TemplateType
|
||||
let version: String?
|
||||
|
||||
enum TemplateType: String {
|
||||
case optionalString = "String?"
|
||||
case string = "String"
|
||||
}
|
||||
|
||||
var value: String {
|
||||
return """
|
||||
// Do not set this variable, it is set during the build process.
|
||||
let VERSION: \(type.rawValue) = \(version ?? "nil")
|
||||
|
||||
"""
|
||||
}
|
||||
|
||||
public static func build(_ version: String? = nil) -> String {
|
||||
Self(type: .string, version: version).value
|
||||
}
|
||||
|
||||
public static func optional(_ version: String? = nil) -> String {
|
||||
Self(type: .optionalString, version: version).value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,4 +276,5 @@ let VERSION: String = nil
|
||||
enum CliClientError: Error {
|
||||
case gitDirectoryNotFound
|
||||
case fileExists(path: String)
|
||||
case failedToParseVersionFile
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ public struct FileClient: Sendable {
|
||||
public var read: @Sendable (URL) throws -> String
|
||||
|
||||
/// Write `Data` to a file `URL`.
|
||||
public private(set) var write: @Sendable (Data, URL) throws -> Void
|
||||
public var write: @Sendable (Data, URL) throws -> Void
|
||||
|
||||
/// Read the contents of a file at the given path.
|
||||
///
|
||||
|
||||
@@ -119,7 +119,7 @@ private struct GitVersion {
|
||||
shell: .env,
|
||||
environment: nil,
|
||||
in: workingDirectory ?? FileManager.default.currentDirectoryPath,
|
||||
argument.arguments
|
||||
argument.arguments.map(\.rawValue)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
106
Tests/CliVersionTests/CliClientTests.swift
Normal file
106
Tests/CliVersionTests/CliClientTests.swift
Normal file
@@ -0,0 +1,106 @@
|
||||
@_spi(Internal) import CliVersion
|
||||
import Dependencies
|
||||
import Foundation
|
||||
import Testing
|
||||
import TestSupport
|
||||
|
||||
@Suite("CliClientTests")
|
||||
struct CliClientTests {
|
||||
|
||||
@Test(
|
||||
arguments: TestTarget.testCases
|
||||
)
|
||||
func testBuild(target: String) async throws {
|
||||
try await run {
|
||||
let client = CliClient.liveValue
|
||||
|
||||
let output = try await client.build(.init(
|
||||
gitDirectory: "/baz",
|
||||
dryRun: false,
|
||||
fileName: "foo",
|
||||
target: target,
|
||||
verbose: true
|
||||
))
|
||||
|
||||
#expect(output == "/baz/Sources/bar/foo")
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
arguments: CliClient.BumpOption.allCases
|
||||
)
|
||||
func bump(type: CliClient.BumpOption) async throws {
|
||||
try await run {
|
||||
$0.fileClient.read = { _ in Template.optional("1.0.0") }
|
||||
} operation: {
|
||||
let client = CliClient.liveValue
|
||||
let output = try await client.bump(
|
||||
type,
|
||||
.init(
|
||||
gitDirectory: "/baz",
|
||||
dryRun: false,
|
||||
fileName: "foo",
|
||||
target: "bar",
|
||||
verbose: true
|
||||
)
|
||||
)
|
||||
|
||||
#expect(output == "/baz/Sources/bar/foo")
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
arguments: TestTarget.testCases
|
||||
)
|
||||
func generate(target: String) async throws {
|
||||
// let (stream, continuation) = AsyncStream<Data>.makeStream()
|
||||
try await run {
|
||||
let client = CliClient.liveValue
|
||||
let output = try await client.generate(.init(
|
||||
gitDirectory: "/baz",
|
||||
dryRun: false,
|
||||
fileName: "foo",
|
||||
target: target,
|
||||
verbose: true
|
||||
))
|
||||
#expect(output == "/baz/Sources/bar/foo")
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
arguments: TestTarget.testCases
|
||||
)
|
||||
func update(target: String) async throws {
|
||||
// let (stream, continuation) = AsyncStream<Data>.makeStream()
|
||||
try await run {
|
||||
let client = CliClient.liveValue
|
||||
let output = try await client.update(.init(
|
||||
gitDirectory: "/baz",
|
||||
dryRun: false,
|
||||
fileName: "foo",
|
||||
target: target,
|
||||
verbose: true
|
||||
))
|
||||
#expect(output == "/baz/Sources/bar/foo")
|
||||
}
|
||||
}
|
||||
|
||||
func run(
|
||||
setupDependencies: @escaping (inout DependencyValues) -> Void = { _ in },
|
||||
operation: @Sendable @escaping () async throws -> Void
|
||||
) async throws {
|
||||
try await withDependencies {
|
||||
$0.logger.logLevel = .debug
|
||||
$0.fileClient = .noop
|
||||
$0.fileClient.fileExists = { _ in false }
|
||||
$0.gitVersionClient = .init { _ in "1.0.0" }
|
||||
setupDependencies(&$0)
|
||||
} operation: {
|
||||
try await operation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum TestTarget {
|
||||
static let testCases = ["bar", "Sources/bar", "/Sources/bar", "./Sources/bar"]
|
||||
}
|
||||
Reference in New Issue
Block a user