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" : [ "pins" : [
{ {
"identity" : "combine-schedulers", "identity" : "combine-schedulers",
@@ -95,8 +96,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/m-housh/swift-shell-client.git", "location" : "https://github.com/m-housh/swift-shell-client.git",
"state" : { "state" : {
"revision" : "9c0c4757d0a2e313d1a4a28e60418a753d3e4230", "revision" : "ea819f41b87aa94e792f028a031f5db786e79a94",
"version" : "0.1.4" "version" : "0.2.1"
} }
}, },
{ {
@@ -118,5 +119,5 @@
} }
} }
], ],
"version" : 2 "version" : 3
} }

View File

@@ -5,7 +5,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "swift-cli-version", name: "swift-cli-version",
platforms: [ platforms: [
.macOS(.v10_15) .macOS(.v13)
], ],
products: [ products: [
.library(name: "CliVersion", targets: ["CliVersion"]), .library(name: "CliVersion", targets: ["CliVersion"]),
@@ -15,7 +15,7 @@ let package = Package(
], ],
dependencies: [ dependencies: [
.package(url: "https://github.com/pointfreeco/swift-dependencies.git", from: "1.6.2"), .package(url: "https://github.com/pointfreeco/swift-dependencies.git", from: "1.6.2"),
.package(url: "https://github.com/m-housh/swift-shell-client.git", from: "0.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-docc-plugin.git", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.2") .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. /// Handles the command-line commands.
@DependencyClient @DependencyClient
public struct CliClient { public struct CliClient: Sendable {
/// Build and update the version based on the git tag, or branch + sha. /// 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. /// 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. /// 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 { extension CliClient: DependencyKey {
@@ -33,9 +63,10 @@ extension CliClient: DependencyKey {
public static func live(environment: [String: String]) -> Self { public static func live(environment: [String: String]) -> Self {
.init( .init(
build: { try $0.run(environment) }, build: { try $0.build(environment) },
generate: { try $0.run() }, bump: { try $1.bump($0) },
update: { try $0.run() } 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 // MARK: Private
@_spi(Internal) @_spi(Internal)
public extension CliClient.SharedOptions { public extension CliClient.SharedOptions {
var fileUrl: URL { 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 { var url = url(for: gitDirectory ?? (targetHasSources ? target : "Sources"))
let targetUrl = fileUrl if gitDirectory != nil {
.deletingLastPathComponent() if !targetHasSources {
.deletingLastPathComponent() url.appendPathComponent("Sources")
}
guard targetUrl.lastPathComponent == "Sources" else { url.appendPathComponent(target)
return url(for: "Sources")
.appendingPathComponent(target)
.appendingPathComponent(fileName)
} }
return fileUrl url.appendPathComponent(fileName)
return url
} }
@discardableResult @discardableResult
@@ -134,10 +107,10 @@ public extension CliClient.SharedOptions {
} }
} }
private extension CliClient.BuildOptions { private extension CliClient.SharedOptions {
func run(_ environment: [String: String]) throws -> String { func build(_ environment: [String: String]) throws -> String {
try shared.run { try run {
@Dependency(\.gitVersionClient) var gitVersion @Dependency(\.gitVersionClient) var gitVersion
@Dependency(\.fileClient) var fileClient @Dependency(\.fileClient) var fileClient
@Dependency(\.logger) var logger @Dependency(\.logger) var logger
@@ -150,28 +123,74 @@ private extension CliClient.BuildOptions {
logger.debug("Building with git directory: \(gitDirectory)") logger.debug("Building with git directory: \(gitDirectory)")
let fileUrl = shared.fileUrl let fileUrl = self.fileUrl
logger.debug("File url: \(fileUrl.cleanFilePath)") logger.debug("File url: \(fileUrl.cleanFilePath)")
let currentVersion = try gitVersion.currentVersion(in: gitDirectory) let currentVersion = try gitVersion.currentVersion(in: gitDirectory)
let fileContents = buildTemplate let fileContents = Template.build(currentVersion)
.replacingOccurrences(of: "nil", with: "\"\(currentVersion)\"")
try fileClient.write(string: fileContents, to: fileUrl) try fileClient.write(string: fileContents, to: fileUrl)
return fileUrl.cleanFilePath return fileUrl.cleanFilePath
} }
} }
}
private extension CliClient.GenerateOptions { func bump(_ type: CliClient.BumpOption) throws -> String {
func run() throws -> String {
@Dependency(\.fileClient) var fileClient @Dependency(\.fileClient) var fileClient
@Dependency(\.logger) var logger @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)") logger.debug("Generate target url: \(targetUrl.cleanFilePath)")
@@ -179,37 +198,68 @@ private extension CliClient.GenerateOptions {
throw CliClientError.fileExists(path: targetUrl.cleanFilePath) throw CliClientError.fileExists(path: targetUrl.cleanFilePath)
} }
if !shared.dryRun { let template = Template.optional(version)
try fileClient.write(string: optionalTemplate, to: targetUrl)
if !dryRun {
try fileClient.write(string: template, to: targetUrl)
} else { } else {
logger.debug("Skipping, due to dry-run being passed.") logger.debug("Skipping, due to dry-run being passed.")
} }
return targetUrl.cleanFilePath 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 { func bump(
@Dependency(\.fileClient) var fileClient major: inout Int,
@Dependency(\.gitVersionClient) var gitVersionClient minor: inout Int,
@Dependency(\.logger) var logger patch: inout Int
) {
let targetUrl = try shared.parseTarget() switch self {
logger.debug("Target url: \(targetUrl.cleanFilePath)") case .major:
major += 1
let currentVersion = try gitVersionClient.currentVersion(in: gitDirectory) minor = 0
patch = 0
let fileContents = optionalTemplate case .minor:
.replacingOccurrences(of: "nil", with: "\"\(currentVersion)\"") minor += 1
patch = 0
if !shared.dryRun { case .patch:
try fileClient.write(string: fileContents, to: targetUrl) patch += 1
} else {
logger.debug("Skipping due to dry run being passed.")
logger.debug("Parsed version: \(currentVersion)")
} }
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 { enum CliClientError: Error {
case gitDirectoryNotFound case gitDirectoryNotFound
case fileExists(path: String) case fileExists(path: String)
case failedToParseVersionFile
} }

View File

@@ -33,7 +33,7 @@ public struct FileClient: Sendable {
public var read: @Sendable (URL) throws -> String public var read: @Sendable (URL) throws -> String
/// Write `Data` to a file `URL`. /// 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. /// Read the contents of a file at the given path.
/// ///

View File

@@ -119,7 +119,7 @@ private struct GitVersion {
shell: .env, shell: .env,
environment: nil, environment: nil,
in: workingDirectory ?? FileManager.default.currentDirectoryPath, 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"]
}