Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.9.5] - 2025-10-19

### Fixed
- Formula files are now correctly published to Formula subfolder within tap directory
- Remove-formula command now properly deletes formula files from Formula subfolder

## [0.9.4] - 2025-10-18

### Fixed
Expand Down
10 changes: 9 additions & 1 deletion Sources/NnexKit/Extensions/String+Homebrew.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,12 @@ public extension String {

return String(self.dropFirst("homebrew-".count))
}
}

func appendingPathComponent(_ path: String) -> String {
if self.hasSuffix("/") {
return self + path
}

return self + "/" + path
}
}
7 changes: 4 additions & 3 deletions Sources/NnexKit/Utilities/FormulaPublisher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,13 @@ public extension FormulaPublisher {
func publishFormula(_ content: String, formulaName: String, commitMessage: String?, tapFolderPath: String) throws -> String {
let fileName = "\(formulaName).rb"
let tapFolder = try Folder(path: tapFolderPath)
let formulaFolder = try tapFolder.createSubfolderIfNeeded(withName: "Formula")

if tapFolder.containsFile(named: fileName) {
try tapFolder.file(named: fileName).delete()
if formulaFolder.containsFile(named: fileName) {
try formulaFolder.file(named: fileName).delete()
}

let newFile = try tapFolder.createFile(named: fileName)
let newFile = try formulaFolder.createFile(named: fileName)
try newFile.write(content)

if let commitMessage {
Expand Down
4 changes: 2 additions & 2 deletions Sources/nnex/Commands/Brew/RemoveFormula.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ extension Nnex.Brew {
let formulas = try context.loadFormulas()
let selection = try picker.requiredSingleSelection(title: "Select a formula to remove", items: formulas)

if let tap = selection.tap, let tapFolder = try? Folder(path: tap.localPath), let formulaFile = try? tapFolder.file(named: "\(selection.name).rb"), picker.getPermission(prompt: "Would you also like to delete the formula file for \(selection.name)?") {
if let tap = selection.tap, let tapFolder = try? Folder(path: tap.localPath), let formulaFolder = try? tapFolder.subfolder(named: "Formula"), let formulaFile = try? formulaFolder.file(named: "\(selection.name).rb"), picker.getPermission(prompt: "Would you also like to delete the formula file for \(selection.name)?") {

try formulaFile.delete()
}

Expand Down
18 changes: 13 additions & 5 deletions Tests/NnexKitTests/FormulaPublisherTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,11 @@ extension FormulaPublisherTests {
let sut = makeSUT().sut
let formulaFile = try requireFormulaFile(sut: sut)
let savedContents = try formulaFile.readAsString()
let formulaFolder = try getFormulaFolder()

#expect(savedContents == content)
#expect(formulaFile.name == formulaFileName)
#expect(tapFolder.containsFile(named: formulaFileName))
#expect(formulaFolder.containsFile(named: formulaFileName))
}

@Test("Overwrites existing formula file")
Expand All @@ -65,8 +66,10 @@ extension FormulaPublisherTests {

try requireFormulaFile(sut: sut)

let formulaFolder = try getFormulaFolder()

#expect(gitHandler.message == nil)
#expect(tapFolder.files.count() == 1)
#expect(formulaFolder.files.count() == 1)
}

@Test("Commits changes and pushes tap folder when a commit message is provided")
Expand All @@ -76,8 +79,10 @@ extension FormulaPublisherTests {

try requireFormulaFile(sut: sut, commitMessage: commitMessage)

let formulaFolder = try getFormulaFolder()

#expect(gitHandler.message == commitMessage)
#expect(tapFolder.files.count() == 1)
#expect(formulaFolder.files.count() == 1)
}
}

Expand All @@ -95,11 +100,14 @@ private extension FormulaPublisherTests {

// MARK: - Helpers
private extension FormulaPublisherTests {
func getFormulaFolder() throws -> Folder {
return try tapFolder.subfolder(named: "Formula")
}

@discardableResult
func requireFormulaFile(sut: FormulaPublisher, commitMessage: String? = nil) throws -> File {
let path = try sut.publishFormula(content, formulaName: formulaName, commitMessage: commitMessage, tapFolderPath: tapFolder.path)
let file = try File(path: path)

return file
return try .init(path: path)
}
}
12 changes: 8 additions & 4 deletions Tests/nnexTests/PublishTests/PublishTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ extension PublishTests {
try createTestTapAndFormula(factory: factory)
try runCommand(factory, version: .version(versionNumber), message: commitMessage, notes: releaseNotes, buildType: .universal)

let formulaFileContents = try Folder(path: tapFolder.path).file(named: formulaFileName).readAsString()
let formulaFileContents = try getFormulaFolder().file(named: formulaFileName).readAsString()

#expect(formulaFileContents.contains(projectName.capitalized))
#expect(formulaFileContents.contains(sha256))
Expand All @@ -86,7 +86,7 @@ extension PublishTests {
let args = ["brew", "publish", "-p", projectFolderWithDashes.path, "-v", versionNumber, "-m", commitMessage, "-n", releaseNotes]
try Nnex.testRun(contextFactory: factory, args: args)

let formulaFileContents = try Folder(path: tapFolder.path).file(named: formulaFileNameWithDashes).readAsString()
let formulaFileContents = try getFormulaFolder().file(named: formulaFileNameWithDashes).readAsString()

#expect(formulaFileContents.contains("class \(expectedClassName)"))
#expect(!formulaFileContents.contains("class \(projectWithDashes)"))
Expand Down Expand Up @@ -243,7 +243,7 @@ extension PublishTests {

// MARK: - Input Provided from User
extension PublishTests {
@Test("Publishes a binary to Homebrew and verifies the formula file when infomation must be input and file path for release notes is input.")
@Test("Publishes a binary to Homebrew and verifies the formula file when infomation must be input and file path for release notes is input.")
func publishCommandWithInputsAndFilePathReleaseNotes() throws {
let releaseNoteFile = try projectFolder.createFile(named: "TestReleaseNotes.md")
let filePath = releaseNoteFile.path
Expand All @@ -257,7 +257,7 @@ extension PublishTests {
try runCommand(factory)

let releaseNoteInfo = try #require(gitHandler.releaseNoteInfo)
let formulaFileContents = try Folder(path: tapFolder.path).file(named: formulaFileName).readAsString()
let formulaFileContents = try getFormulaFolder().file(named: formulaFileName).readAsString()

#expect(formulaFileContents.contains(projectName.capitalized))
#expect(formulaFileContents.contains(sha256))
Expand Down Expand Up @@ -322,6 +322,10 @@ private extension PublishTests {

// MARK: - Helpers
private extension PublishTests {
func getFormulaFolder() throws -> Folder {
return try tapFolder.subfolder(named: "Formula")
}

func createMockShell(projectName: String? = nil, projectPath: String? = nil, includeTestCommand: Bool = false, shouldThrowError: Bool = false) -> MockShell {
if shouldThrowError {
return .init(shouldThrowErrorOnFinal: true)
Expand Down
138 changes: 138 additions & 0 deletions Tests/nnexTests/RemoveFormulaTests/RemoveFormulaTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
//
// RemoveFormulaTests.swift
// nnex
//
// Created by Nikolai Nobadi on 3/31/25.
//

import NnexKit
import Testing
import Foundation
import NnexSharedTestHelpers
@testable import nnex
@preconcurrency import Files

@MainActor
final class RemoveFormulaTests {
private let tapName = "testTap"
private let tempFolder: Folder
private let tapFolder: Folder

init() throws {
self.tempFolder = try Folder.temporary.createSubfolder(named: "RemoveFormulaTests_\(UUID().uuidString)")
self.tapFolder = try tempFolder.createSubfolder(named: tapName.homebrewTapName)
}

deinit {
deleteFolderContents(tempFolder)
try? tempFolder.delete()
}
}


// MARK: - Unit Tests
extension RemoveFormulaTests {
@Test("ensures no formulas exist in database")
func startingValuesEmpty() throws {
let testFactory = MockContextFactory()
let context = try testFactory.makeContext()

#expect(try context.loadFormulas().isEmpty)
}

@Test("Removes formula from database")
func removesFormulaFromDatabase() throws {
let name = "testFormula"
let testFactory = MockContextFactory(tapListFolderPath: tempFolder.path)

try createTestTapAndFormula(factory: testFactory, formulaName: name)

let context = try testFactory.makeContext()
let formulasBeforeRemoval = try context.loadFormulas()
#expect(formulasBeforeRemoval.count == 1)

try runCommand(testFactory)

let formulasAfterRemoval = try context.loadFormulas()
#expect(formulasAfterRemoval.isEmpty)
}

@Test("Deletes formula file from Formula folder when user confirms")
func deletesFormulaFileWhenUserConfirms() throws {
let name = "testFormula"
let testFactory = MockContextFactory(tapListFolderPath: tempFolder.path, permissionResponses: [true])

try createTestTapAndFormula(factory: testFactory, formulaName: name, createFormulaFile: true)

let formulaFolder = try tapFolder.subfolder(named: "Formula")
#expect(formulaFolder.containsFile(named: "\(name).rb"))

try runCommand(testFactory)

let updatedFormulaFolder = try Folder(path: formulaFolder.path)
#expect(!updatedFormulaFolder.containsFile(named: "\(name).rb"))

let context = try testFactory.makeContext()
#expect(try context.loadFormulas().isEmpty)
}

@Test("Keeps formula file in Formula folder when user declines deletion")
func keepsFormulaFileWhenUserDeclines() throws {
let name = "testFormula"
let testFactory = MockContextFactory(tapListFolderPath: tempFolder.path, permissionResponses: [false])

try createTestTapAndFormula(factory: testFactory, formulaName: name, createFormulaFile: true)

let formulaFolder = try tapFolder.subfolder(named: "Formula")
#expect(formulaFolder.containsFile(named: "\(name).rb"))

try runCommand(testFactory)

let updatedFormulaFolder = try Folder(path: formulaFolder.path)
#expect(updatedFormulaFolder.containsFile(named: "\(name).rb"))

let context = try testFactory.makeContext()
#expect(try context.loadFormulas().isEmpty)
}

@Test("Handles missing Formula folder gracefully")
func handlesMissingFormulaFolder() throws {
let name = "testFormula"
let testFactory = MockContextFactory(tapListFolderPath: tempFolder.path, permissionResponses: [true])

try createTestTapAndFormula(factory: testFactory, formulaName: name, createFormulaFile: false)

let context = try testFactory.makeContext()
let formulasBeforeRemoval = try context.loadFormulas()
#expect(formulasBeforeRemoval.count == 1)

try runCommand(testFactory)

let formulasAfterRemoval = try context.loadFormulas()
#expect(formulasAfterRemoval.isEmpty)
}
}


// MARK: - Helper Methods
private extension RemoveFormulaTests {
func runCommand(_ testFactory: MockContextFactory) throws {
let args = ["brew", "remove-formula"]
try Nnex.testRun(contextFactory: testFactory, args: args)
}

func createTestTapAndFormula(factory: MockContextFactory, formulaName: String, createFormulaFile: Bool = false) throws {
let context = try factory.makeContext()
let tap = SwiftDataTap(name: tapName, localPath: tapFolder.path, remotePath: "https://github.com/user/\(tapName)")
let formula = SwiftDataFormula(name: formulaName, details: "formula details", homepage: "https://github.com/user/\(formulaName)", license: "MIT", localProjectPath: "/path/to/project", uploadType: .binary, testCommand: nil, extraBuildArgs: [])

try context.saveNewTap(tap, formulas: [formula])

if createFormulaFile {
let formulaFolder = try tapFolder.createSubfolderIfNeeded(withName: "Formula")
let formulaContent = FormulaContentGenerator.makeFormulaFileContent(name: formulaName, details: "formula details", homepage: "https://github.com/user/\(formulaName)", license: "MIT", assetURL: "https://example.com/asset", sha256: "abc123")
let formulaFile = try formulaFolder.createFile(named: "\(formulaName).rb")
try formulaFile.write(formulaContent)
}
}
}