From fb6a07eef08c12de356b8f1f27b54f8b370f8018 Mon Sep 17 00:00:00 2001 From: Adam Wulf Date: Sat, 25 Oct 2025 22:11:16 -0500 Subject: [PATCH] Add CommitOptions to support empty commits --- .../Models/Options/CommitOptions.swift | 26 ++++++++++++++++ Sources/SwiftGitX/Repository.swift | 9 ++++-- .../RepositoryOperationTests.swift | 30 +++++++++++++++++++ 3 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 Sources/SwiftGitX/Models/Options/CommitOptions.swift diff --git a/Sources/SwiftGitX/Models/Options/CommitOptions.swift b/Sources/SwiftGitX/Models/Options/CommitOptions.swift new file mode 100644 index 0000000..0a533ed --- /dev/null +++ b/Sources/SwiftGitX/Models/Options/CommitOptions.swift @@ -0,0 +1,26 @@ +import libgit2 + +/// Options for the commit operation. +public struct CommitOptions { + public static let `default` = CommitOptions() + + public static let allowEmpty = CommitOptions(allowEmpty: true) + + /// If true, allow creating a commit with no changes. Otherwise, fail if there are no changes. Default is false. + public let allowEmpty: Bool + + public init(allowEmpty: Bool = false) { + self.allowEmpty = allowEmpty + } + + var gitCommitCreateOptions: git_commit_create_options { + var options = git_commit_create_options() + options.version = UInt32(GIT_COMMIT_CREATE_OPTIONS_VERSION) + options.allow_empty_commit = allowEmpty ? 1 : 0 + options.author = nil + options.committer = nil + options.message_encoding = nil + + return options + } +} diff --git a/Sources/SwiftGitX/Repository.swift b/Sources/SwiftGitX/Repository.swift index 88ac7ce..1fb108c 100644 --- a/Sources/SwiftGitX/Repository.swift +++ b/Sources/SwiftGitX/Repository.swift @@ -503,7 +503,9 @@ public extension Repository { public extension Repository { /// Create a new commit containing the current contents of the index. /// - /// - Parameter message: The commit message. + /// - Parameters: + /// - message: The commit message. + /// - options: The options to use when creating the commit. /// /// - Returns: The created commit. /// @@ -511,15 +513,16 @@ public extension Repository { /// /// This method uses the default author and committer information. @discardableResult - func commit(message: String) throws -> Commit { + func commit(message: String, options: CommitOptions = .default) throws -> Commit { // Create a new commit from the index var oid = git_oid() + var gitOptions = options.gitCommitCreateOptions let status = git_commit_create_from_stage( &oid, pointer, message, - nil + &gitOptions ) guard status == GIT_OK.rawValue else { diff --git a/Tests/SwiftGitXTests/RepositoryTests/RepositoryOperationTests.swift b/Tests/SwiftGitXTests/RepositoryTests/RepositoryOperationTests.swift index fec731a..b2f4b11 100644 --- a/Tests/SwiftGitXTests/RepositoryTests/RepositoryOperationTests.swift +++ b/Tests/SwiftGitXTests/RepositoryTests/RepositoryOperationTests.swift @@ -46,6 +46,36 @@ final class RepositoryOperationTests: SwiftGitXTestCase { XCTAssertEqual(commit, headCommit) } + func testEmptyCommit() throws { + // Create a new repository at the temporary directory + let repository = Repository.mock(named: "test-empty-commit", in: Self.directory) + + // Create initial commit + try repository.mockCommit(message: "Initial commit") + + // Verify that committing with no changes fails by default + XCTAssertThrowsError(try repository.commit(message: "Empty commit without option")) { error in + // Expect a failedToCommit error + if case RepositoryError.failedToCommit = error { + // Expected error + } else { + XCTFail("Expected failedToCommit error, got \(error)") + } + } + + // Verify that committing with allowEmpty option succeeds + let emptyCommit = try repository.commit(message: "Empty commit with option", options: .allowEmpty) + + // Get the HEAD commit + let headCommit = try XCTUnwrap(repository.HEAD.target as? Commit) + + // Check if the HEAD commit is the same as the created empty commit + XCTAssertEqual(emptyCommit, headCommit) + + // Verify the commit message + XCTAssertEqual(emptyCommit.message, "Empty commit with option") + } + func testReset() throws { // Create a new repository at the temporary directory let repository = Repository.mock(named: "test-reset", in: Self.directory)