feat: Working on bump version.

This commit is contained in:
2024-12-20 23:02:22 -05:00
parent 2ca83829e0
commit b7ac6ee9d1
6 changed files with 274 additions and 116 deletions

View File

@@ -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
}

View File

@@ -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")
],

View File

@@ -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
}
}
private extension CliClient.UpdateOptions {
func run() throws -> String {
@Dependency(\.fileClient) var fileClient
func update() throws -> String {
@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)")
return try generate(gitVersionClient.currentVersion(in: gitDirectory))
}
return targetUrl.cleanFilePath
}
@_spi(Internal)
public extension CliClient.BumpOption {
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
}
}
}
@_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
}

View File

@@ -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.
///

View File

@@ -119,7 +119,7 @@ private struct GitVersion {
shell: .env,
environment: nil,
in: workingDirectory ?? FileManager.default.currentDirectoryPath,
argument.arguments
argument.arguments.map(\.rawValue)
)
}
}

View 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"]
}