diff --git a/Makefile b/Makefile index dda11f6..29a17ec 100644 --- a/Makefile +++ b/Makefile @@ -28,4 +28,10 @@ test-linux: swift:5.7-focal \ swift test +update-version: + swift package \ + --disable-sandbox \ + --allow-writing-to-package-directory \ + update-version \ + git-version diff --git a/Package.swift b/Package.swift index 87117c2..52aac02 100644 --- a/Package.swift +++ b/Package.swift @@ -9,7 +9,7 @@ let package = Package( ], products: [ .library(name: "GitVersion", targets: ["GitVersion"]), - .plugin(name: "GenerateVersionBuildPlugin", targets: ["GenerateVersionBuildPlugin"]), + .plugin(name: "BuildWithVersionPlugin", targets: ["BuildWithVersionPlugin"]), .plugin(name: "GenerateVersionPlugin", targets: ["GenerateVersionPlugin"]), .plugin(name: "UpdateVersionPlugin", targets: ["UpdateVersionPlugin"]) ], @@ -37,7 +37,7 @@ let package = Package( dependencies: ["GitVersion"] ), .plugin( - name: "GenerateVersionBuildPlugin", + name: "BuildWithVersionPlugin", capability: .buildTool(), dependencies: [ "git-version" diff --git a/Plugins/GenerateVersionBuildPlugin/GenerateVersionBuildPlugin.swift b/Plugins/BuildWithVersionPlugin/BuildWithVersionPlugin.swift similarity index 100% rename from Plugins/GenerateVersionBuildPlugin/GenerateVersionBuildPlugin.swift rename to Plugins/BuildWithVersionPlugin/BuildWithVersionPlugin.swift diff --git a/Sources/GitVersion/Documentation.docc/Articles/GettingStarted.md b/Sources/GitVersion/Documentation.docc/Articles/GettingStarted.md new file mode 100644 index 0000000..397418e --- /dev/null +++ b/Sources/GitVersion/Documentation.docc/Articles/GettingStarted.md @@ -0,0 +1,51 @@ +# Getting Started + +Learn how to integrate the plugins into your project + +## Overview + +Use the plugins by including as a package to your project and declaring in the `plugins` section of +your target. + +```swift +let package = Package( + ..., + dependencies: [ + ..., + .package(url: "https://github.com/m-housh/swift-git-version.git", from: "0.1.0") + ], + targets: [ + .executableTarget( + name: "", + dependencies: [...], + plugins: [ + .plugin(name: "BuildWithVersionPlugin", package: "swift-git-version") + ] + ) + ] +) +``` + +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 + +```swift +import ArgumentParser + +@main +struct MyCliTool: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "My awesome cli tool", + version: VERSION + ) + + func run() throws { + print("Version: \(VERSION)") + } +} +``` + +You will have access to the `VERSION` string variable even though it is not declared in your source +files. diff --git a/Sources/GitVersion/Documentation.docc/GitVersion.md b/Sources/GitVersion/Documentation.docc/GitVersion.md new file mode 100644 index 0000000..54fd22d --- /dev/null +++ b/Sources/GitVersion/Documentation.docc/GitVersion.md @@ -0,0 +1,22 @@ +# ``GitVersion`` + +Derive a version for a command line tool from git tags or a git sha. + +## Additional Resources + +[Github Repo](https://github.com/m-housh/swift-git-version) + +## 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. + +## Articles + +- + +### Api + +- ``FileClient`` +- ``GitVersionClient`` diff --git a/Sources/GitVersion/FileClient.swift b/Sources/GitVersion/FileClient.swift index 684de2d..2a1d7a4 100644 --- a/Sources/GitVersion/FileClient.swift +++ b/Sources/GitVersion/FileClient.swift @@ -14,10 +14,6 @@ import XCTestDynamicOverlay /// public struct FileClient { - /// Read the file contents from the given `URL` as `Data`. - /// - public private(set) var read: (URL) throws -> Data - /// Write `Data` to a file `URL`. public private(set) var write: (Data, URL) throws -> Void @@ -30,91 +26,13 @@ public struct FileClient { ///``` /// /// - Parameters: - /// - read: Read the file contents. /// - write: Write the data to a file. public init( - read: @escaping (URL) throws -> Data, write: @escaping (Data, URL) throws -> Void ) { - self.read = read self.write = write } - /// Read a file at the given path. - /// - /// - Parameters: - /// - path: The path to read the file at. - public func read(path: String) throws -> Data { - let url = try url(for: path) - return try self.read(url) - } - - /// Read the file as a string. - /// - /// - Parameters: - /// - url: The url for the file. - public func readAsString(url: URL) throws -> String { - let data = try read(url) - return String(decoding: data, as: UTF8.self) - } - - /// Read the file as a string - /// - /// - Parameters: - /// - path: The file path to read. - public func readAsString(path: String) throws -> String { - try self.readAsString(url: url(for: path)) - } - - /// Read the contents of a file and decode as the decodable type. - /// -/// - Parameters: - /// - decodable: The type to decode. - /// - url: The file url. - /// - decoder: The decoder to use. - public func read( - _ decodable: D.Type, - from url: URL, - using decoder: JSONDecoder = .init() - ) throws -> D { - let data = try read(url) - return try decoder.decode(D.self, from: data) - } - - /// Read the contents of a file and decode as the decodable type. - /// - /// - Parameters: - /// - decodable: The type to decode. - /// - path: The file path. - /// - decoder: The decoder to use. - public func read( - _ decodable: D.Type, - from path: String, - using decoder: JSONDecoder = .init() - ) throws -> D { - let data = try read(path: path) - return try decoder.decode(D.self, from: data) - } - - /// Write the data to a file at the given path. - /// - /// - Parameters: - /// - data: The data to write to the file. - /// - path: The file path. - public func write(data: Data, to path: String) throws { - let url = try url(for: path) - try self.write(data, url) - } - - /// Write's the given string to a file. - /// - /// - Parameters: - /// - string: The string to write to the file. - /// - url: The file url. - public func write(string: String, to url: URL) throws { - try self.write(Data(string.utf8), url) - } - /// Write's the the string to a file path. /// /// - Parameters: @@ -124,25 +42,31 @@ public struct FileClient { let url = try url(for: path) try self.write(string: string, to: url) } + + /// Write's the the string to a file path. + /// + /// - Parameters: + /// - string: The string to write to the file. + /// - url: The file url. + public func write(string: String, to url: URL) throws { + try self.write(Data(string.utf8), url) + } } extension FileClient: DependencyKey { /// A ``FileClient`` that does not do anything. public static let noop = FileClient.init( - read: { _ in Data() }, write: { _, _ in } ) /// An `unimplemented` ``FileClient``. public static let testValue = FileClient( - read: unimplemented("\(Self.self).read", placeholder: Data()), write: unimplemented("\(Self.self).write") ) /// The live ``FileClient`` public static let liveValue = FileClient( - read: { try Data(contentsOf: $0) }, write: { try $0.write(to: $1, options: .atomic) } ) @@ -158,20 +82,6 @@ extension DependencyValues { } } -// MARK: - Overrides -extension FileClient { - - /// Override the data that get's returned when a `read` operation is called. - /// - /// This is useful in a testing context. - /// - /// - Parameters: - /// - data: The data to return when a read operation is called. - public mutating func overrideRead(data: Data) { - self.read = { _ in data } - } -} - // MARK: - Private fileprivate func url(for path: String) throws -> URL { #if os(Linux) diff --git a/Sources/GitVersion/GitVersionClient.swift b/Sources/GitVersion/GitVersionClient.swift index f91bfcf..20a09a3 100644 --- a/Sources/GitVersion/GitVersionClient.swift +++ b/Sources/GitVersion/GitVersionClient.swift @@ -10,23 +10,10 @@ import XCTestDynamicOverlay /// point to a commit that is tagged, it will use the `branch git-sha` as /// the version. /// -/// This is often not used directly, instead it is used with ``SwiftBuild``er +/// This is often not used directly, instead it is used with one of the plugins /// that is supplied with this library. The use case is to set the version of a command line /// tool based on the current git tag. /// -/// ```swift -/// @main -/// public struct Build { -/// public static func main() throws { -/// @Dependency(\.shellClient) var shell: ShellClient -/// -/// try shell.replacingNilWithVersionString( -/// in: "Sources/example/Version.swift", -/// build: SwiftBuild.release() -/// ) -/// } -/// } -/// ``` public struct GitVersionClient { /// The closure to run that returns the current version from a given @@ -92,6 +79,20 @@ extension DependencyValues { } } +extension ShellCommand { + public static func gitCurrentSha(gitDirectory: String? = nil) -> Self { + GitVersion(workingDirectory: gitDirectory).command(for: .commit) + } + + public static func gitCurrentBranch(gitDirectory: String? = nil) -> Self { + GitVersion(workingDirectory: gitDirectory).command(for: .branch) + } + + public static func gitCurrentTag(gitDirectory: String? = nil) -> Self { + GitVersion(workingDirectory: gitDirectory).command(for: .describe) + } +} + // MARK: - Private fileprivate struct GitVersion { @Dependency(\.logger) var logger: Logger diff --git a/Sources/git-version/BuildCommand.swift b/Sources/git-version/BuildCommand.swift index 741e3ee..de60511 100644 --- a/Sources/git-version/BuildCommand.swift +++ b/Sources/git-version/BuildCommand.swift @@ -38,7 +38,7 @@ extension GitVersionCommand { let currentVersion = try gitVersion.currentVersion(in: gitDirectory) - let fileContents = template + let fileContents = buildTemplate .replacingOccurrences(of: "nil", with: "\"\(currentVersion)\"") try fileClient.write(string: fileContents, to: fileUrl) diff --git a/Sources/git-version/GenerateCommand.swift b/Sources/git-version/GenerateCommand.swift index 9e7c77d..db86cde 100644 --- a/Sources/git-version/GenerateCommand.swift +++ b/Sources/git-version/GenerateCommand.swift @@ -9,7 +9,8 @@ extension GitVersionCommand { struct Generate: ParsableCommand { static var 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." + discussion: "This command can be interacted with directly, outside of the plugin usage context.", + version: VERSION ?? "0.0.0" ) @OptionGroup var shared: SharedOptions @@ -29,7 +30,7 @@ extension GitVersionCommand { } if !shared.dryRun { - try fileClient.write(string: template, to: fileUrl) + try fileClient.write(string: optionalTemplate, to: fileUrl) logger.info("Generated file at: \(fileString)") } else { logger.info("Would generate file at: \(fileString)") diff --git a/Sources/git-version/Helpers.swift b/Sources/git-version/Helpers.swift index 2fa742c..da31d29 100644 --- a/Sources/git-version/Helpers.swift +++ b/Sources/git-version/Helpers.swift @@ -20,12 +20,18 @@ extension URL { } } -let template = """ +let optionalTemplate = """ // Do not set this variable, it is set during the build process. let VERSION: String? = nil """ +let buildTemplate = """ +// Do not set this variable, it is set during the build process. +let VERSION: String = nil + +""" + struct SharedOptions: ParsableArguments { @Argument(help: "The target for the version file.") diff --git a/Sources/git-version/UpdateCommand.swift b/Sources/git-version/UpdateCommand.swift index 4cbceb0..791857e 100644 --- a/Sources/git-version/UpdateCommand.swift +++ b/Sources/git-version/UpdateCommand.swift @@ -39,7 +39,7 @@ extension GitVersionCommand { let currentVersion = try gitVersion.currentVersion(in: gitDirectory) - let fileContents = template + let fileContents = optionalTemplate .replacingOccurrences(of: "nil", with: "\"\(currentVersion)\"") if !shared.dryRun { diff --git a/Sources/git-version/Version.swift b/Sources/git-version/Version.swift index c5554ab..5564f2c 100644 --- a/Sources/git-version/Version.swift +++ b/Sources/git-version/Version.swift @@ -1,2 +1,2 @@ // Do not set this variable, it is set during the build process. -let VERSION: String? = nil +let VERSION: String? = "main 3dc5d8f" diff --git a/Tests/GitVersionTests/GitVersionTests.swift b/Tests/GitVersionTests/GitVersionTests.swift index af2800c..20195a3 100644 --- a/Tests/GitVersionTests/GitVersionTests.swift +++ b/Tests/GitVersionTests/GitVersionTests.swift @@ -4,6 +4,26 @@ import ShellClient final class GitVersionTests: XCTestCase { + override func invokeTest() { + withDependencies({ + $0.logger.logLevel = .debug + $0.logger = .liveValue + $0.shellClient = .liveValue + $0.gitVersionClient = .liveValue + $0.fileClient = .liveValue + }, operation: { + super.invokeTest() + }) + } + + var gitDir: String { + URL(fileURLWithPath: #file) + .deletingLastPathComponent() + .deletingLastPathComponent() + .deletingLastPathComponent() + .absoluteString + } + func test_overrides_work() throws { try withDependencies { $0.gitVersionClient.override(with: "blob") @@ -16,29 +36,66 @@ final class GitVersionTests: XCTestCase { } func test_live() throws { - try withDependencies({ - $0.logger.logLevel = .debug - $0.logger = .liveValue - $0.shellClient = .liveValue - $0.gitVersionClient = .liveValue - }, operation: { - @Dependency(\.gitVersionClient) var versionClient + @Dependency(\.gitVersionClient) var versionClient - let gitDir = URL(fileURLWithPath: #file) - .deletingLastPathComponent() - .deletingLastPathComponent() - .deletingLastPathComponent() + let version = try versionClient.currentVersion(in: gitDir) + print("VERSION: \(version)") + // can't really have a predictable result for the live client. + XCTAssertNotEqual(version, "blob") - let version = try versionClient.currentVersion(in: gitDir.absoluteString) - print("VERSION: \(version)") - // can't really have a predictable result for the live client. - XCTAssertNotEqual(version, "blob") + } -// print(FileManager.default.currentDirectoryPath) -// let other = try versionClient.currentVersion() -// XCTAssertEqual(version, other) + func test_commands() throws { + @Dependency(\.shellClient) var shellClient - }) + let branch = try shellClient.background( + .gitCurrentBranch(gitDirectory: gitDir), + trimmingCharactersIn: .whitespacesAndNewlines + ) + XCTAssertEqual(branch, "main") + + let commit = try shellClient.background( + .gitCurrentSha(gitDirectory: gitDir), + trimmingCharactersIn: .whitespacesAndNewlines + ) + XCTAssertNotEqual(commit, "") + + } + + func test_file_client() throws { + @Dependency(\.fileClient) var fileClient + + let tmpDir = FileManager.default.temporaryDirectory + .appendingPathComponent("file-client-test") + + try FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: true) + defer { try! FileManager.default.removeItem(at: tmpDir) } + + let filePath = tmpDir.appendingPathComponent("blob.txt") + try fileClient.write(string: "Blob", to: filePath) + + let contents = try String(contentsOf: filePath) + .trimmingCharacters(in: .whitespacesAndNewlines) + XCTAssertEqual(contents, "Blob") + + } + + func test_file_client_with_string_path() throws { + @Dependency(\.fileClient) var fileClient + + let tmpDir = FileManager.default.temporaryDirectory + .appendingPathComponent("file-client-string-test") + + try FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: true) + defer { try! FileManager.default.removeItem(at: tmpDir) } + + let filePath = tmpDir.appendingPathComponent("blob.txt") + let fileString = filePath.absoluteString.replacingOccurrences(of: "file://", with: "") + try fileClient.write(string: "Blob", to: fileString) + + let contents = try String(contentsOf: filePath) + .trimmingCharacters(in: .whitespacesAndNewlines) + XCTAssertEqual(contents, "Blob") } }