diff --git a/CHANGELOG.md b/CHANGELOG.md index 156587e37cd..fe5ed046fe5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ Note: This is in reverse chronological order, so newer entries are added to the Swift Next ----------- +* [#7202] + + Package manifests can now access information about the Git repository the given package is in via the context object's + `gitInformation` property. This allows to determine the current tag (if any), the current commit and whether or not there are uncommited changes. + * [#7010] On macOS, `swift build` and `swift run` now produce binaries that allow backtraces in debug builds. Pass `SWIFT_BACKTRACE=enable=yes` environment variable to enable backtraces on such binaries when running them. diff --git a/Package.swift b/Package.swift index 39c6823061b..52ad8af68d0 100644 --- a/Package.swift +++ b/Package.swift @@ -224,7 +224,8 @@ let package = Package( name: "PackageLoading", dependencies: [ "Basics", - "PackageModel" + "PackageModel", + "SourceControl", ], exclude: ["CMakeLists.txt", "README.md"] ), diff --git a/Sources/PackageDescription/Context.swift b/Sources/PackageDescription/Context.swift index 612b3855529..a931d880628 100644 --- a/Sources/PackageDescription/Context.swift +++ b/Sources/PackageDescription/Context.swift @@ -22,7 +22,19 @@ public struct Context { public static var packageDirectory : String { model.packageDirectory } - + + /// Information about the git status of a given package, if available. + @available(_PackageDescription, introduced: 5.11) + public static var gitInformation: GitInformation? { + model.gitInformation.map { + GitInformation( + currentTag: $0.currentTag, + currentCommit: $0.currentCommit, + hasUncommittedChanges: $0.hasUncommittedChanges + ) + } + } + /// Snapshot of the system environment variables. public static var environment : [String : String] { model.environment @@ -31,3 +43,11 @@ public struct Context { private init() { } } + +/// Information about the git status of a given package, if available. +@available(_PackageDescription, introduced: 5.11) +public struct GitInformation { + public let currentTag: String? + public let currentCommit: String + public let hasUncommittedChanges: Bool +} diff --git a/Sources/PackageLoading/ContextModel.swift b/Sources/PackageLoading/ContextModel.swift index 90bd7b50765..c01935e76a6 100644 --- a/Sources/PackageLoading/ContextModel.swift +++ b/Sources/PackageLoading/ContextModel.swift @@ -18,14 +18,17 @@ import Foundation struct ContextModel { let packageDirectory : String - - init(packageDirectory : String) { - self.packageDirectory = packageDirectory - } + let gitInformation: GitInformation? var environment : [String : String] { ProcessInfo.processInfo.environment } + + struct GitInformation: Codable { + let currentTag: String? + let currentCommit: String + let hasUncommittedChanges: Bool + } } extension ContextModel : Codable { diff --git a/Sources/PackageLoading/ManifestLoader.swift b/Sources/PackageLoading/ManifestLoader.swift index 99bf6e161a2..9ec7dc99a98 100644 --- a/Sources/PackageLoading/ManifestLoader.swift +++ b/Sources/PackageLoading/ManifestLoader.swift @@ -15,6 +15,7 @@ import Basics import Dispatch import Foundation import PackageModel +import SourceControl import class TSCBasic.BufferedOutputByteStream import struct TSCBasic.ByteString @@ -958,7 +959,23 @@ public final class ManifestLoader: ManifestLoaderProtocol { do { let packageDirectory = manifestPath.parentDirectory.pathString - let contextModel = ContextModel(packageDirectory: packageDirectory) + + let gitInformation: ContextModel.GitInformation? + do { + let repo = GitRepository(path: manifestPath.parentDirectory) + gitInformation = ContextModel.GitInformation( + currentTag: repo.getCurrentTag(), + currentCommit: try repo.getCurrentRevision().identifier, + hasUncommittedChanges: repo.hasUncommittedChanges() + ) + } catch { + gitInformation = nil + } + + let contextModel = ContextModel( + packageDirectory: packageDirectory, + gitInformation: gitInformation + ) cmd += ["-context", try contextModel.encode()] } catch { return completion(.failure(error)) diff --git a/Sources/PackageModel/ToolsVersion.swift b/Sources/PackageModel/ToolsVersion.swift index 427399d0255..5fe51fe4bf4 100644 --- a/Sources/PackageModel/ToolsVersion.swift +++ b/Sources/PackageModel/ToolsVersion.swift @@ -31,6 +31,7 @@ public struct ToolsVersion: Equatable, Hashable, Codable, Sendable { public static let v5_8 = ToolsVersion(version: "5.8.0") public static let v5_9 = ToolsVersion(version: "5.9.0") public static let v5_10 = ToolsVersion(version: "5.10.0") + public static let v5_11 = ToolsVersion(version: "5.11.0") public static let vNext = ToolsVersion(version: "999.0.0") /// The current tools version in use. diff --git a/Sources/SourceControl/GitRepository.swift b/Sources/SourceControl/GitRepository.swift index 2781413db10..8859982421f 100644 --- a/Sources/SourceControl/GitRepository.swift +++ b/Sources/SourceControl/GitRepository.swift @@ -623,6 +623,17 @@ public final class GitRepository: Repository, WorkingCheckout { } } + public func getCurrentTag() -> String? { + self.lock.withLock { + try? callGit( + "describe", + "--exact-match", + "--tags", + failureMessage: "Couldn’t get current tag" + ) + } + } + public func checkout(tag: String) throws { // FIXME: Audit behavior with off-branch tags in remote repositories, we // may need to take a little more care here. diff --git a/Tests/PackageLoadingTests/PD_5_11_LoadingTests.swift b/Tests/PackageLoadingTests/PD_5_11_LoadingTests.swift new file mode 100644 index 00000000000..36b4c8a6f91 --- /dev/null +++ b/Tests/PackageLoadingTests/PD_5_11_LoadingTests.swift @@ -0,0 +1,91 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Basics +import PackageModel +import SourceControl +import SPMTestSupport +import XCTest + +class PackageDescription5_11LoadingTests: PackageDescriptionLoadingTests { + override var toolsVersion: ToolsVersion { + .v5_11 + } + + func testPackageContextGitStatus() throws { + let content = """ + import PackageDescription + let package = Package(name: "\\(Context.gitInformation?.hasUncommittedChanges == true)") + """ + + try loadRootManifestWithBasicGitRepository(manifestContent: content) { manifest, observability in + XCTAssertNoDiagnostics(observability.diagnostics) + XCTAssertEqual(manifest.displayName, "true") + } + } + + func testPackageContextGitTag() throws { + let content = """ + import PackageDescription + let package = Package(name: "\\(Context.gitInformation?.currentTag ?? "")") + """ + + try loadRootManifestWithBasicGitRepository(manifestContent: content) { manifest, observability in + XCTAssertNoDiagnostics(observability.diagnostics) + XCTAssertEqual(manifest.displayName, "lunch") + } + } + + func testPackageContextGitCommit() throws { + let content = """ + import PackageDescription + let package = Package(name: "\\(Context.gitInformation?.currentCommit ?? "")") + """ + + try loadRootManifestWithBasicGitRepository(manifestContent: content) { manifest, observability in + XCTAssertNoDiagnostics(observability.diagnostics) + + let repo = GitRepository(path: manifest.path.parentDirectory) + let currentRevision = try repo.getCurrentRevision() + XCTAssertEqual(manifest.displayName, currentRevision.identifier) + } + } + + private func loadRootManifestWithBasicGitRepository( + manifestContent: String, + validator: (Manifest, TestingObservability) throws -> () + ) throws { + let observability = ObservabilitySystem.makeForTesting() + + try testWithTemporaryDirectory { tmpdir in + let manifestPath = tmpdir.appending(component: Manifest.filename) + try localFileSystem.writeFileContents(manifestPath, string: manifestContent) + try localFileSystem.writeFileContents(tmpdir.appending("best.txt"), string: "best") + + let repo = GitRepository(path: tmpdir) + try repo.create() + try repo.stage(file: manifestPath.pathString) + try repo.commit(message: "best") + try repo.tag(name: "lunch") + + let manifest = try manifestLoader.load( + manifestPath: manifestPath, + packageKind: .root(tmpdir), + toolsVersion: self.toolsVersion, + fileSystem: localFileSystem, + observabilityScope: observability.topScope + ) + + try validator(manifest, observability) + } + } +}