diff --git a/Package.resolved b/Package.resolved index fa56055..af186bd 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,6 +1,15 @@ { "object": { "pins": [ + { + "package": "Consler", + "repositoryURL": "https://github.com/enuance/consler", + "state": { + "branch": null, + "revision": "440cda5566fdabae879cbb5cef4576a21dea7d4c", + "version": "0.2.0" + } + }, { "package": "Files", "repositoryURL": "https://github.com/JohnSundell/Files", diff --git a/Package.swift b/Package.swift index bd2945c..b9302c5 100644 --- a/Package.swift +++ b/Package.swift @@ -9,12 +9,14 @@ let package = Package( // 📁 John Sundell's Files Package is great for easy file reading/writing/moving/etc. .package(url: "https://github.com/JohnSundell/Files", from: "4.0.0"), // 🧰 SPMUtilities for CLI Argument Parsing. - .package(url: "https://github.com/apple/swift-package-manager", from: "0.5.0") + .package(url: "https://github.com/apple/swift-package-manager", from: "0.5.0"), + // Consler for Styled outputs to the Console + .package(url: "https://github.com/enuance/consler", from: "0.2.0") ], targets: [ .target( name: "commitPrefix", - dependencies: ["Files", "SPMUtility"], + dependencies: ["Files", "SPMUtility", "Consler"], // Normally don't have to specify the path, but I wan't the actual executable to be // lowercase and SPM brings folders in Uppercased by default. path: "Sources/CommitPrefix"), diff --git a/Sources/CommitPrefix/CPFileHandler.swift b/Sources/CommitPrefix/CPFileHandler.swift index 4586a6d..58a0542 100644 --- a/Sources/CommitPrefix/CPFileHandler.swift +++ b/Sources/CommitPrefix/CPFileHandler.swift @@ -24,6 +24,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +import Consler import Foundation import Files @@ -40,40 +41,40 @@ struct CPFileHandler { try CommitMessageHook.findOrCreate(with: gitDirectory) } - func outputPrefixes() throws -> String { + func outputPrefixes() throws -> ConslerOutput { try cpInteractor.outputPrefixes() } - func viewState() throws -> String { + func viewState() throws -> ConslerOutput { let cpState = try cpInteractor.getCommitPrefixState() switch cpState.mode { case .normal: - return """ - CommitPrefix MODE NORMAL - - prefixes: \(cpState.normalPrefixes.joined()) - """ + return ConslerOutput(values: [ + "CommitPrefix ", "MODE NORMAL", + "- prefixes: ", cpState.normalPrefixes.joined()]) + .describedBy(.normal, .cyanEndsLine, .normal, .cyan) case .branchParse: - return """ - CommitPrefix MODE BRANCH_PARSE - - branch prefixes: \(cpState.branchPrefixes.joined()) - - stored prefixes: \(cpState.normalPrefixes.joined()) - """ + return ConslerOutput(values: [ + "CommitPrefix ", "MODE BRANCH_PARSE", + "- branch prefixes: ", cpState.branchPrefixes.joined(), + "- stored prefixes: ", cpState.normalPrefixes.joined()]) + .describedBy(.normal, .cyanEndsLine, .normal, .cyanEndsLine, .normal, .cyan) } } - func deletePrefixes() throws -> String { + func deletePrefixes() throws -> ConslerOutput { try cpInteractor.deletePrefixes() } - func writeNew(prefixes rawValue: String) throws -> String { + func writeNew(prefixes rawValue: String) throws -> ConslerOutput { try cpInteractor.writeNew(prefixes: rawValue) } - func activateBranchMode(with validator: String) throws -> String { + func activateBranchMode(with validator: String) throws -> ConslerOutput { try cpInteractor.activateBranchMode(with: validator) } - func activateNormalMode() throws -> String { + func activateNormalMode() throws -> ConslerOutput { try cpInteractor.activateNormalMode() } diff --git a/Sources/CommitPrefix/CPInteractor.swift b/Sources/CommitPrefix/CPInteractor.swift index 0a2476a..0a846b5 100644 --- a/Sources/CommitPrefix/CPInteractor.swift +++ b/Sources/CommitPrefix/CPInteractor.swift @@ -24,8 +24,9 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import Foundation +import Consler import Files +import Foundation struct CPInteractor { @@ -53,7 +54,7 @@ struct CPInteractor { return (cpFile, cpModel, headFile) } catch { cpDebugPrint(error) - throw CPError.fileReadWriteError + throw CPError.cpFileIOError } } @@ -64,24 +65,24 @@ struct CPInteractor { try commitPrefixFile.write(modelData) } catch { cpDebugPrint(error) - throw CPError.fileReadWriteError + throw CPError.cpFileIOError } } private func branchPrefixes() throws -> [String] { guard let regexValue = commitPrefixModel.regexValue else { - throw CPTermination.branchValidatorNotPresent + throw CPError.branchValidatorNotFound } guard let branch = try? gitHEADFile.readAsString(encodedAs: .utf8) else { - throw CPTermination.unableToReadHEAD + throw CPError.headFileIOError } let matches = branch.occurances(ofRegex: regexValue) guard matches.count > 0 else { let validator = commitPrefixModel.branchValidator ?? "Validator Not Present" - throw CPTermination.invalidBranchPrefix(validator: validator) + throw CPError.invalidBranchPrefix(validator: validator) } let uniqueMatches = Set(matches) @@ -101,20 +102,20 @@ struct CPInteractor { let containsNoNumbers = validator.occurances(ofRegex: #"(\d+)"#).isEmpty let atLeastTwoCharacters = validator.count > 1 guard containsNoNumbers && atLeastTwoCharacters else { - throw CPError.branchValidatorFormatError + throw CPError.invalidBranchValidatorFormat } return validator } - func outputPrefixes() throws -> String { + func outputPrefixes() throws -> ConslerOutput { switch commitPrefixModel.prefixMode { case .normal: - return commitPrefixModel.prefixes.joined() + return ConslerOutput(commitPrefixModel.prefixes.joined()) case .branchParse: let retrievedBranchPrefixes = try branchPrefixes() let branchPrefixes = retrievedBranchPrefixes.map { "[\($0)]" }.joined() let normalPrefixes = commitPrefixModel.prefixes.joined() - return branchPrefixes + normalPrefixes + return ConslerOutput(branchPrefixes, normalPrefixes) } } @@ -138,34 +139,37 @@ struct CPInteractor { } } - func deletePrefixes() throws -> String { + func deletePrefixes() throws -> ConslerOutput { let newModel = commitPrefixModel.updated(with: []) try saveCommitPrefix(model: newModel) - return "CommitPrefix DELETED" + return ConslerOutput("CommitPrefix ", "DELETED").describedBy(.normal, .red) } - func writeNew(prefixes rawValue: String) throws -> String { + func writeNew(prefixes rawValue: String) throws -> ConslerOutput { let newPrefixes = prefixFormatter(rawValue) let newModel = commitPrefixModel.updated(with: newPrefixes) try saveCommitPrefix(model: newModel) - return "CommitPrefix STORED \(newPrefixes.joined())" + return ConslerOutput("CommitPrefix ", "STORED ", newPrefixes.joined()) + .describedBy(.normal, .green, .green) } - func activateBranchMode(with validator: String) throws -> String { + func activateBranchMode(with validator: String) throws -> ConslerOutput { let formattedValidator = try validatorFormatter(validator) let newModel = commitPrefixModel.updatedAsBranchMode(with: formattedValidator) try saveCommitPrefix(model: newModel) - return "CommitPrefix MODE BRANCH_PARSE \(formattedValidator)" + return ConslerOutput("CommitPrefix ","MODE BRANCH_PARSE ", formattedValidator) + .describedBy(.normal, .cyan, .green) } - func activateNormalMode() throws -> String { + func activateNormalMode() throws -> ConslerOutput { switch commitPrefixModel.prefixMode { case .normal: - return "CommitPrefix already in MODE NORMAL" + return ConslerOutput("CommitPrefix ", "already in ", "MODE NORMAL") + .describedBy(.normal, .yellow, .cyan) case .branchParse: let newModel = commitPrefixModel.updatedAsNormalMode() try saveCommitPrefix(model: newModel) - return "CommitPrefix MODE NORMAL" + return ConslerOutput("CommitPrefix ", "MODE NORMAL").describedBy(.normal, .cyan) } } diff --git a/Sources/CommitPrefix/Constants.swift b/Sources/CommitPrefix/Constants.swift index 09748ca..dc864cb 100644 --- a/Sources/CommitPrefix/Constants.swift +++ b/Sources/CommitPrefix/Constants.swift @@ -28,7 +28,7 @@ import Foundation struct CPInfo { - static let version = "1.3.2" + static let version = "1.4.0" } diff --git a/Sources/CommitPrefix/Error+Debug/CPError.swift b/Sources/CommitPrefix/Error+Debug/CPError.swift index f919169..73f8584 100644 --- a/Sources/CommitPrefix/Error+Debug/CPError.swift +++ b/Sources/CommitPrefix/Error+Debug/CPError.swift @@ -24,71 +24,158 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +import Consler import Foundation +enum TerminationStatus: Int32 { + /// Used when the app finishes as expected + case successful + + /// Used when an error that has not been accounted for has been thrown + case unexpectedError + + /// Used when the user takes an action that stops the application short + case userInitiated + + /// Used when the inputs provided to the app are invalid + case invalidInputs + + /// Used when the app can no longer continue due to user specified settings + case invalidContext + + /// Used when required resources are inaccessible or unavailable + case unavailableDependencies + + var value: Int32 { self.rawValue } +} + enum CPError: Error { - case userCommandNotRecognized - case newEntryShouldNotHaveSpaces + // MARK: - CLI Errors + case commandNotRecognized + case tooManyArguments case emptyEntry - case multipleArguments + + // MARK: - User Termination Errors + case overwriteCancelled + + // MARK: - Format Errors + case invalidEntryFormat + case invalidBranchValidatorFormat + case invalidBranchPrefix(validator: String) + case invalidYesOrNoFormat + + // MARK: - Dependency Location Errors case notAGitRepo(currentLocation: String) - case fileReadWriteError case directoryNotFound(name: String, path: String) - case hookReadWriteError - case branchValidatorFormatError + case branchValidatorNotFound + + // MARK: - Read/Write Errors + case cpFileIOError + case hookFileIOError + case headFileIOError - var message: String { + var message: ConslerOutput { switch self { - case .userCommandNotRecognized: - return "Command not recognized. Enter \"--help\" for usage." - case .newEntryShouldNotHaveSpaces: - return "Your entry contains invalid spaces." + + case .commandNotRecognized: + return ConslerOutput( + "Error: ", "Command not recognized. Enter ", "\"--help\"", " for usage.") + .describedBy(.boldRed, .normal, .cyan) + + case .tooManyArguments: + return ConslerOutput( + "Error: ", "Too many arguments entered. Only two at a time is supported.") + .describedBy(.boldRed) + case .emptyEntry: - return "Your entry is empty." - case .multipleArguments: - return "Too many arguments entered. Only two at a time is supported." + return ConslerOutput("Error: ", "Your entry is empty.").describedBy(.boldRed) + + case .overwriteCancelled: + return ConslerOutput("Error: ", "Overwrite is cancelled").describedBy(.boldRed) + + case .invalidEntryFormat: + return ConslerOutput("Error: ", "Your entry contains invalid spaces.") + .describedBy(.boldRed) + + case .invalidBranchValidatorFormat: + return ConslerOutput( + "Error: ", "The branch validator must be at least two characters long ", + "and contain no numbers or spaces") + .describedBy(.boldRed) + + case .invalidBranchPrefix(validator: let validator): + return ConslerOutput( + "Error: ", "Your branch does not begin with", " \(validator)", " and is invalid.", + "Either: ", "change your branch name", " or ", "use commitPrefix in MODE NORMAL.") + .describedBy(.boldRed, .normal, .yellow, .endsLine, .normal, .cyan, .normal, .cyan) + + case .invalidYesOrNoFormat: + return ConslerOutput("Error: ", "Expected y or n. The transaction has been cancelled.") + .describedBy(.boldRed) + case .notAGitRepo(currentLocation: let location): - return "Not in a git repo or at the root of one: \(location)" - case .fileReadWriteError: - return "An error occured while reading or writing to the CommitPrefix files" + return ConslerOutput( + "Error: ", "Not in a git repo or at the root of one: ", "\(location)") + .describedBy(.boldRed, .normal, .yellow) + case .directoryNotFound(name: let name, path: let path): - return "Directory named \(name) was not found at \(path)" - case .hookReadWriteError: - return "An error occured while reading or writing to the commit-msg hook" - case .branchValidatorFormatError: - return "The branch validator must be at least two characters long " - + "and contain no numbers or spaces" + return ConslerOutput( + "Error: ", "Directory named ", "\(name)", " was not found at ", "\(path)") + .describedBy(.boldRed, .normal, .yellow, .normal, .yellow) + + case .branchValidatorNotFound: + return ConslerOutput( + "Error: ", "Attempting to provide a branch prefix without a branch validator") + .describedBy(.boldRed) + + case .cpFileIOError: + return ConslerOutput( + "Error: ", "An error occured while reading or writing to the CommitPrefix files") + .describedBy(.boldRed) + + case .hookFileIOError: + return ConslerOutput( + "Error: ", "An error occured while reading or writing to the commit-msg hook") + .describedBy(.boldRed) + + case .headFileIOError: + return ConslerOutput("Error: ", "Unable to read the git HEAD for branch information") + .describedBy(.boldRed) } } -} - -/// An Error Type that should terminate the program if detected -enum CPTermination: Error { - - case overwriteCancelled - case expectedYesOrNo - case branchValidatorNotPresent - case invalidBranchPrefix(validator: String) - case unableToReadHEAD - - var message: String { + var status: TerminationStatus { switch self { + case .commandNotRecognized: + return .invalidInputs + case .tooManyArguments: + return .invalidInputs + case .emptyEntry: + return .invalidInputs case .overwriteCancelled: - return "Overwrite is cancelled" - case .expectedYesOrNo: - return "Expected y or n. The transaction has been cancelled." - case .branchValidatorNotPresent: - return "Attempting to provide a branch prefix without a branch validator" - case .invalidBranchPrefix(validator: let validator): - return """ - Your branch does not begin with \(validator) and is invalid. - Either change your branch name or use commitPrefix in non-branch mode. - """ - case .unableToReadHEAD: - return "Unable to read the git HEAD for branch information" + return .userInitiated + case .invalidEntryFormat: + return .invalidInputs + case .invalidBranchValidatorFormat: + return .invalidInputs + case .invalidBranchPrefix: + return .invalidContext + case .invalidYesOrNoFormat: + return .invalidInputs + case .notAGitRepo: + return .unavailableDependencies + case .directoryNotFound: + return .unavailableDependencies + case .branchValidatorNotFound: + return .unavailableDependencies + case .cpFileIOError: + return .unavailableDependencies + case .hookFileIOError: + return .unavailableDependencies + case .headFileIOError: + return .unavailableDependencies } } diff --git a/Sources/CommitPrefix/Hook/CommitMessageHook.swift b/Sources/CommitPrefix/Hook/CommitMessageHook.swift index 2fb6171..e35c74b 100644 --- a/Sources/CommitPrefix/Hook/CommitMessageHook.swift +++ b/Sources/CommitPrefix/Hook/CommitMessageHook.swift @@ -24,6 +24,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +import Consler import Files import Foundation @@ -62,7 +63,7 @@ struct CommitMessageHook { cpDebugPrint(commitHookFile.path) Shell.makeExecutable(commitHookFile.path) } catch { - throw CPError.hookReadWriteError + throw CPError.hookFileIOError } return nil @@ -74,27 +75,32 @@ struct CommitMessageHook { } private func overwriteCommitHook(_ commitHookFile: File) throws { - print("There seems to be an existing commit-msg found in the hooks directory") - print("Would you like to overwrite? [y/n]") + Consler.output( + ["", "There seems to be an existing commit-msg found in the hooks directory", + "- Would you like to overwrite? [y/n]", ""], + descriptors: [.endsLine, .yellowEndsLine, .yellow]) + let answer = readLine() ?? "" switch answer { case "y": - print("Overwritting existing commit-msg with generated hook") + Consler.output( + ["","Overwriting existing commit-msg with generated hook", ""], + descriptors: [.endsLine, .cyanEndsLine]) do { // TODO: - Theres a case where the file is not executable in the first place this will not correct that try commitHookFile.write(cmHookContents.renderScript(), encoding: .utf8) } catch { - throw CPError.hookReadWriteError + throw CPError.hookFileIOError } case "n": - throw CPTermination.overwriteCancelled + throw CPError.overwriteCancelled default: - throw CPTermination.expectedYesOrNo + throw CPError.invalidYesOrNoFormat } } @@ -102,7 +108,7 @@ struct CommitMessageHook { private func hookIsCommitPrefix(_ hookFile: File) throws -> Bool { guard let hookContents = try? hookFile.readAsString(encodedAs: .utf8) else { - throw CPError.hookReadWriteError + throw CPError.hookFileIOError } return hookContents.contains(cmHookContents.fileIdentifier) diff --git a/Sources/CommitPrefix/Hook/CommitMessageHookContents.swift b/Sources/CommitPrefix/Hook/CommitMessageHookContents.swift index 92cb6c8..753a42b 100644 --- a/Sources/CommitPrefix/Hook/CommitMessageHookContents.swift +++ b/Sources/CommitPrefix/Hook/CommitMessageHookContents.swift @@ -60,6 +60,7 @@ struct CommitMessageHookContents { case invalidArgument case overwriteError + case commitPrefixError var message: String { switch self { @@ -67,6 +68,11 @@ struct CommitMessageHookContents { return "Intended to recieve .git/COMMIT_EDITMSG arg" case .overwriteError: return "There was an error writting to the commit message" + case .commitPrefixError: + return \"\"\" + + - CommitPrefix Error + \"\"\" } } @@ -95,7 +101,7 @@ struct CommitMessageHookContents { } private func renderIOCPMethodGetPrefixes() -> String { """ - func getPrefixes() -> String { + func getPrefixes() throws -> String { let readProcess = Process() readProcess.launchPath = "/usr/bin/env" @@ -112,6 +118,10 @@ struct CommitMessageHookContents { readProcess.waitUntilExit() + if readProcess.terminationStatus != 0 { + throw IOError.commitPrefixError + } + let data = pipe.fileHandleForReading.readDataToEndOfFile() let contents = String(data: data, encoding: .utf8) @@ -156,7 +166,7 @@ struct CommitMessageHookContents { let ioCommitPrefix = try IOCommitPrefix() - let prefixes = ioCommitPrefix.getPrefixes() + let prefixes = try ioCommitPrefix.getPrefixes() .trimmingCharacters(in: .newlines) let commitMessage = ioCommitPrefix.getCommitMessage() @@ -167,8 +177,9 @@ struct CommitMessageHookContents { } catch let ioError as IOError { - print(ioError) - + print(ioError.message) + exit(1) + } """ } diff --git a/Sources/CommitPrefix/Interface/CLIArguments.swift b/Sources/CommitPrefix/Interface/CLIArguments.swift index 26cd26d..597571b 100644 --- a/Sources/CommitPrefix/Interface/CLIArguments.swift +++ b/Sources/CommitPrefix/Interface/CLIArguments.swift @@ -76,7 +76,7 @@ struct CLIArguments { private func singleCommandParse(_ allCommands: [ParsedCommand]) throws -> UserCommand { precondition(allCommands.count == 1, "Intended for single Parsed Command only!") guard let foundCommand = allCommands.first else { - throw CPError.userCommandNotRecognized + throw CPError.commandNotRecognized } switch foundCommand { @@ -91,7 +91,7 @@ struct CLIArguments { case .userEntry(value: let prefixes): return .newPrefixes(value: prefixes) default: - throw CPError.userCommandNotRecognized + throw CPError.commandNotRecognized } } @@ -106,13 +106,13 @@ struct CLIArguments { case (.userEntry(value: let validator), .modeBranchParse): return .modeBranchParse(validator: validator) default: - throw CPError.userCommandNotRecognized + throw CPError.commandNotRecognized } } func getCommand() throws -> UserCommand { guard let parsedArgs = try? parser.parse(rawArgs) else { - throw CPError.userCommandNotRecognized + throw CPError.commandNotRecognized } var allCommands = [ParsedCommand]() @@ -125,7 +125,7 @@ struct CLIArguments { try parsedArgs.get(userEntry).map { userEntry in let noMoreThanOneEntry = userEntry.count < 2 - guard noMoreThanOneEntry else { throw CPError.newEntryShouldNotHaveSpaces } + guard noMoreThanOneEntry else { throw CPError.invalidEntryFormat } guard let theEntry = userEntry.first else { throw CPError.emptyEntry } allCommands.append(.userEntry(value: theEntry)) } @@ -138,7 +138,7 @@ struct CLIArguments { case 2: return try doubleCommandParse(allCommands) default: - throw CPError.multipleArguments + throw CPError.tooManyArguments } } diff --git a/Sources/CommitPrefix/main.swift b/Sources/CommitPrefix/main.swift index 4bb1576..b76fd93 100644 --- a/Sources/CommitPrefix/main.swift +++ b/Sources/CommitPrefix/main.swift @@ -24,6 +24,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +import Consler import Foundation let cpCommandLineInterface = CLIArguments() @@ -33,53 +34,54 @@ do { switch try cpCommandLineInterface.getCommand() { case .outputVersion: - let version = CPInfo.version - print("commitPrefix version \(version)") + Consler.output( + "CommitPrefix ", "version ", CPInfo.version, + descriptors: [.normal, .cyan, .cyan]) case .viewState: let fileHandler = try CPFileHandler() - let currentState = try fileHandler.viewState() - print(currentState) + let viewStateOutput = try fileHandler.viewState() + Consler.output(viewStateOutput) case .outputPrefixes: let fileHandler = try CPFileHandler() - let prefixOutput = try fileHandler.outputPrefixes() - print(prefixOutput) + let prefixesOutput = try fileHandler.outputPrefixes() + Consler.output(prefixesOutput) case .deletePrefixes: let fileHandler = try CPFileHandler() - let deletionMessage = try fileHandler.deletePrefixes() - print(deletionMessage) + let deletionOutput = try fileHandler.deletePrefixes() + Consler.output(deletionOutput) case .modeNormal: let fileHandler = try CPFileHandler() - let modeSetMessage = try fileHandler.activateNormalMode() - print(modeSetMessage) + let normalModeOutput = try fileHandler.activateNormalMode() + Consler.output(normalModeOutput) case .modeBranchParse(validator: let rawValidatorValue): let fileHandler = try CPFileHandler() - let modeSetMessage = try fileHandler.activateBranchMode(with: rawValidatorValue) - print(modeSetMessage) + let branchModeOutput = try fileHandler.activateBranchMode(with: rawValidatorValue) + Consler.output(branchModeOutput) case .newPrefixes(value: let rawPrefixValue): let fileHandler = try CPFileHandler() - let storedPrefixesMessage = try fileHandler.writeNew(prefixes: rawPrefixValue) - print(storedPrefixesMessage) + let newPrefixesOutput = try fileHandler.writeNew(prefixes: rawPrefixValue) + Consler.output(newPrefixesOutput) } } catch let prefixError as CPError { - print(prefixError.message) - -} catch let terminationError as CPTermination { - - print(terminationError.message) - exit(0) + Consler.output(prefixError.message ,type: .error) + exit(prefixError.status.value) } catch { - print("Unexpected Error: ", error) - exit(0) + Consler.output( + "Unexpected Error: ", error.localizedDescription, + descriptors: [.boldRed, .normal], + type: .error) + + exit(TerminationStatus.unexpectedError.value) }