diff --git a/README.md b/README.md index f109708..baf0414 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,16 @@ ___ CommitPrefix is a simple command line tool that helps you to easily prefix your commit messages. The common use case for this is tagging your commit messages with a Jira (or other issue tracking software) ticket number. The desired prefix is stored within the .git folder and picked up by a generated commit-message hook. This allows you to write your ticket number (or any other prefix) once. From then on all commit messages will be prepended with the saved prefix. +There's also a branch parse mode that allows commitPrefix to parse the current branch you're on for a valid issue numbers and use them as prefixes for your next commit. The modes can be switched back and forth arbitrarily and used along with any self defined prefixes. + Prefixes can be re-assigned or deleted at any time. Additionally, this is a git repository specific tool, meaning that stored prefixes are specific to the repository you're in. The actions that can be done are: -* Store a commit prefix -* Delete the currently stored prefix -* View the currently stored prefix +* Store an arbitrary number of commit prefixes +* Generate prefixes based on your current branch +* Delete the currently stored prefixes +* View the current mode and stored prefixes ___ ### -- Installation -- @@ -68,26 +71,45 @@ To use commitPrefix you need to have your working directory set to one that has To **store** a prefix ```zsh -% commitPrefix SamplePrefix-001 +% commitPrefix SamplePrefix-001,SamplePrefix-002 + +# Output +CommitPrefix STORED [SamplePrefix-001][SamplePrefix-002] +``` + +To change mode to **branchParse** +```zsh +% git checkout ENG-342-SomeFeatureBranchLinkedToENG-101 +% commitPrefix -b eng + +# Output +CommitPrefix MODE BRANCH_PARSE eng +``` + +To **view** the current prefixes and mode +```zsh +% commitPrefix # Output -CommitPrefix saved: [SamplePrefix-001] +CommitPrefix MODE BRANCH_PARSE +- branch prefixes: [SamplePrefix-001][SamplePrefix-002] +- stored prefixes: [ENG-342][ENG-101] ``` -To **view** a prefix +To change back to **normal** mode ```zsh -% commitPrefix --view +% commitPrefix -n # Output -CommitPrefix: [SamplePrefix-001] +CommitPrefix MODE NORMAL ``` To **delete** a prefix ```zsh -% commitPrefix --delete +% commitPrefix -d # Output -CommitPrefix Deleted +CommitPrefix DELETED ``` You can also view these command along with shortend version by using the `--help` tag. diff --git a/Sources/CommitPrefix/CLIArguments.swift b/Sources/CommitPrefix/CLIArguments.swift index 7e9b7c6..e93a919 100644 --- a/Sources/CommitPrefix/CLIArguments.swift +++ b/Sources/CommitPrefix/CLIArguments.swift @@ -30,64 +30,110 @@ import SPMUtility public struct CLIArguments { public enum UserCommand { - case delete - case view - case newEntry(entry: String) + case viewState + case outputPrefixes + case deletePrefixes + case modeNormal + case modeBranchParse(validator: String) + case newPrefixes(value: String) + } + + private enum ParsedCommand { + case viewState + case outputPrefixes + case deletePrefixes + case modeNormal + case modeBranchParse + case userEntry(value: String) } private let parser: ArgumentParser private let rawArgs: [String] - private let delete: OptionArgument - private let view: OptionArgument - private let newEntry: PositionalArgument<[String]> + private let outputPrefixes: OptionArgument + private let deletePrefixes: OptionArgument + private let modeNormal: OptionArgument + private let modeBranchParse: OptionArgument + private let userEntry: PositionalArgument<[String]> public init(arguments: [String] = CommandLine.arguments) { // The first argument specifies the path of the executable file self.rawArgs = Array(arguments.dropFirst()) let argBuilder = ArgumentBuilder() self.parser = argBuilder.buildParser() - self.delete = argBuilder.buildDeleteArgument(parser: parser) - self.view = argBuilder.buildViewArgument(parser: parser) - self.newEntry = argBuilder.buildNewEntryArgument(parser: parser) + + self.outputPrefixes = argBuilder.buildOutputArgument(parser: parser) + self.deletePrefixes = argBuilder.buildDeleteArgument(parser: parser) + self.modeNormal = argBuilder.buildNormalArgument(parser: parser) + self.modeBranchParse = argBuilder.buildBranchParseArgument(parser: parser) + self.userEntry = argBuilder.buildUserEntryArgument(parser: parser) } + 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 + } + + switch foundCommand { + case .outputPrefixes: + return .outputPrefixes + case .deletePrefixes: + return .deletePrefixes + case .modeNormal: + return .modeNormal + case .userEntry(value: let prefixes): + return .newPrefixes(value: prefixes) + default: + throw CPError.userCommandNotRecognized + } + } + + private func doubleCommandParse(_ allCommands: [ParsedCommand]) throws -> UserCommand { + precondition(allCommands.count == 2, "Intended for two Parsed Commands only!") + let firstCommand = allCommands[0] + let secondCommand = allCommands[1] + + switch (firstCommand, secondCommand) { + case (.modeBranchParse, .userEntry(value: let validator)): + return .modeBranchParse(validator: validator) + case (.userEntry(value: let validator), .modeBranchParse): + return .modeBranchParse(validator: validator) + default: + throw CPError.userCommandNotRecognized + } + } + func getCommand() throws -> UserCommand { guard let parsedArgs = try? parser.parse(rawArgs) else { throw CPError.userCommandNotRecognized } - var allCommands = [UserCommand]() + var allCommands = [ParsedCommand]() - parsedArgs.get(delete).map { _ in allCommands.append(.delete) } - parsedArgs.get(view).map { _ in allCommands.append(.view) } - try parsedArgs.get(newEntry).map { userEntry in - - guard userEntry.count < 2 else { - throw CPError.newEntryShouldNotHaveSpaces - } - - guard let theEntry = userEntry.first else { - throw CPError.emptyEntry - } - - guard !theEntry.isEmpty else { - throw CPError.emptyEntry - } - - allCommands.append(.newEntry(entry: theEntry)) - - } + parsedArgs.get(outputPrefixes).map { _ in allCommands.append(.outputPrefixes) } + parsedArgs.get(deletePrefixes).map { _ in allCommands.append(.deletePrefixes) } + parsedArgs.get(modeNormal).map { _ in allCommands.append(.modeNormal) } + parsedArgs.get(modeBranchParse).map { _ in allCommands.append(.modeBranchParse) } - guard allCommands.count < 2 else { - throw CPError.multipleArguments + try parsedArgs.get(userEntry).map { userEntry in + let noMoreThanOneEntry = userEntry.count < 2 + guard noMoreThanOneEntry else { throw CPError.newEntryShouldNotHaveSpaces } + guard let theEntry = userEntry.first else { throw CPError.emptyEntry } + allCommands.append(.userEntry(value: theEntry)) } - guard let command = allCommands.first else { - throw CPError.userCommandNotRecognized + switch allCommands.count { + case 0: + return .viewState + case 1: + return try singleCommandParse(allCommands) + case 2: + return try doubleCommandParse(allCommands) + default: + throw CPError.multipleArguments } - return command } } @@ -95,43 +141,89 @@ public struct CLIArguments { private struct ArgumentBuilder { let usage: String = """ - + [,,...] [-o | --output] [-d | --delete] + [-n | -normal] [ -b | --branchParse ] """ let overview: String = """ - The CommitPrefix stores a desired prefix for your commit messages. - It stores it within the .git folder of the current repository. A - commit-msg hook is also generated and stored within the .git - folder which is used to prefix the commit message. + + The CommitPrefix stores a desired set of prefixes for your commit messages. It + stores it within the .git folder of the current repository. A commit-msg hook is + also generated and stored within the .git folder which is used to prefix the + commit message. + + Modes: + CommitPrefix has two modes, normal and branch parse. + + - NORMAL + example: commitPrefix ,,... + + You can add normal prefixes by entering comma seperated values. These values will + be parsed as prefixes and prepended to future commit messages. + + - BRANCH_PARSE + example commitPrefix -b + + Branch parse mode checks the current branch for an issue number validated by the + value passed in as an argument. For example if you passed in a validator value of + "eng" and your current branch was named ENG-342-SomeFeatureBranchLinkedToENG-101, + commitPrefix will pickup [ENG-342] and [ENG-101] as branch prefixes to be + prepended to you next commit along with any other normal prefixes you might have. + + You can change back to NORMAL mode by entering: + example: commitPrefix -n + + To view the current state of prefixes and mode, enter: + example: commitPrefix """ func buildParser() -> ArgumentParser { ArgumentParser(usage: usage, overview: overview) } - + + func buildOutputArgument(parser: ArgumentParser) -> OptionArgument { + return parser.add( + option: "--output", + shortName: "-o", + kind: Bool.self, + usage: "Outputs the full, formated prefix to standard output", + completion: nil + ) + } + func buildDeleteArgument(parser: ArgumentParser) -> OptionArgument { return parser.add( option: "--delete", shortName: "-d", kind: Bool.self, - usage: "Deletes the stored prefix", + usage: "Deletes the stored prefixes", completion: nil ) } - func buildViewArgument(parser: ArgumentParser) -> OptionArgument { + func buildNormalArgument(parser: ArgumentParser) -> OptionArgument { + return parser.add( + option: "--normal", + shortName: "-n", + kind: Bool.self, + usage: "Sets the mode to NORMAL", + completion: nil + ) + } + + func buildBranchParseArgument(parser: ArgumentParser) -> OptionArgument { return parser.add( - option: "--view", - shortName: "-v", + option: "--branchParse", + shortName: "-b", kind: Bool.self, - usage: "Display the currently stored prefix", + usage: "Sets the mode to BRANCH_PARSE. Requires a validator argument", completion: nil ) } - func buildNewEntryArgument(parser: ArgumentParser) -> PositionalArgument<[String]> { + func buildUserEntryArgument(parser: ArgumentParser) -> PositionalArgument<[String]> { return parser.add( - positional: "NewEntry", + positional: "UserEntry", kind: [String].self, optional: true, usage: nil, diff --git a/Sources/CommitPrefix/CPDebugPrint.swift b/Sources/CommitPrefix/CPDebugPrint.swift index de9115a..929038c 100644 --- a/Sources/CommitPrefix/CPDebugPrint.swift +++ b/Sources/CommitPrefix/CPDebugPrint.swift @@ -35,10 +35,10 @@ private let isDebugMode = false /// A Debug Printer that only prints in debug mode public func cpDebugPrint(_ value: Any, file: String = #file, line: Int = #line, function: String = #function) { guard isDebugMode else { return } - print("/n", "********** Commit Prefix Debug **********") + print("********** Commit Prefix Debug **********") print("File: \(file)") print("Line: \(line)") print("Function: \(function)") print("value: ", value) - print("*****************************************", "/n") + print("*****************************************") } diff --git a/Sources/CommitPrefix/CPError.swift b/Sources/CommitPrefix/CPError.swift index 5365bc3..46165da 100644 --- a/Sources/CommitPrefix/CPError.swift +++ b/Sources/CommitPrefix/CPError.swift @@ -36,7 +36,7 @@ enum CPError: Error { case fileReadWriteError case directoryNotFound(name: String, path: String) case hookReadWriteError - case expectedYesOrNo + case branchValidatorFormatError var message: String { switch self { @@ -47,7 +47,7 @@ enum CPError: Error { case .emptyEntry: return "Your entry is empty." case .multipleArguments: - return "Multiple arguments entered. Only one at a time is supported." + return "Too many arguments entered. Only two at a time is supported." case .notAGitRepo(currentLocation: let location): return "Not in a git repo or at the root of one: \(location)" case .fileReadWriteError: @@ -56,8 +56,35 @@ enum CPError: Error { 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" + } + + } + +} + +enum CPTermination: Error { + + case overwriteCancelled + case expectedYesOrNo + case branchValidatorNotPresent + case invalidBranchPrefix(validator: String) + + var message: String { + switch self { + case .overwriteCancelled: + return "Overwrite is cancelled" case .expectedYesOrNo: - return "expected y or n. The transaction has been cancelled." + 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. + """ } } diff --git a/Sources/CommitPrefix/CPFileHandler.swift b/Sources/CommitPrefix/CPFileHandler.swift index f0c983d..48608ce 100644 --- a/Sources/CommitPrefix/CPFileHandler.swift +++ b/Sources/CommitPrefix/CPFileHandler.swift @@ -29,51 +29,52 @@ import Files public struct CPFileHandler { - private let commitPrefixFile: File - private let commitMessageHook: CommitMessageHook + private let cpInteractor: CPInteractor public init() throws { - let currentDirectory = Folder.current - let correctLocation = currentDirectory.containsSubfolder(named: FolderName.git) - guard correctLocation else { - throw CPError.notAGitRepo(currentLocation: currentDirectory.path) + guard Folder.current.containsSubfolder(named: FolderName.git) else { + throw CPError.notAGitRepo(currentLocation: Folder.current.path) } - let gitDirectory = try currentDirectory.subfolder(named: FolderName.git) - self.commitPrefixFile = try gitDirectory.createFileIfNeeded(withName: FileName.commitPrefix) - self.commitMessageHook = try CommitMessageHook(gitDirectory: gitDirectory) + let gitDirectory = try Folder.current.subfolder(named: FolderName.git) + self.cpInteractor = try CPInteractor(gitDirectory: gitDirectory) + try CommitMessageHook.findOrCreate(with: gitDirectory) } - public func locateOrCreateHook() throws { - try commitMessageHook.locateOrCreateHook() + public func outputPrefixes() throws -> String { + try cpInteractor.outputPrefixes() } - public func outputPrefix() throws -> String { - let contents = try? commitPrefixFile.readAsString(encodedAs: .utf8) - guard let readContents = contents else { - throw CPError.fileReadWriteError + public func viewState() throws -> String { + let cpState = try cpInteractor.getCommitPrefixState() + switch cpState.mode { + case .normal: + return """ + CommitPrefix MODE NORMAL + - prefixes: \(cpState.normalPrefixes.joined()) + """ + case .branchParse: + return """ + CommitPrefix MODE BRANCH_PARSE + - branch prefixes: \(cpState.branchPrefixes.joined()) + - stored prefixes: \(cpState.normalPrefixes.joined()) + """ } - return readContents } - public func deletePrefix() throws -> String { - do { - try commitPrefixFile.write("", encoding: .utf8) - } catch { - throw CPError.fileReadWriteError - } - return "CommitPrefix Deleted" + public func deletePrefixes() throws -> String { + try cpInteractor.deletePrefixes() } - public func writeNew(prefix: String) throws -> String { - let bracketSet = CharacterSet(charactersIn: "[]") - let debracketedPrefix = prefix.trimmingCharacters(in: bracketSet) - let formattedPrefix = "[\(debracketedPrefix)]" - do { - try commitPrefixFile.write(formattedPrefix, encoding: .utf8) - } catch { - throw CPError.fileReadWriteError - } - return formattedPrefix + public func writeNew(prefixes rawValue: String) throws -> String { + try cpInteractor.writeNew(prefixes: rawValue) + } + + public func activateBranchMode(with validator: String) throws -> String { + try cpInteractor.activateBranchMode(with: validator) + } + + public func activateNormalMode() throws -> String { + try cpInteractor.activateNormalMode() } } diff --git a/Sources/CommitPrefix/CPInteractor.swift b/Sources/CommitPrefix/CPInteractor.swift new file mode 100644 index 0000000..99ed5fd --- /dev/null +++ b/Sources/CommitPrefix/CPInteractor.swift @@ -0,0 +1,165 @@ +// +// CPInteractor.swift +// commitPrefix +// +// MIT License +// +// Copyright (c) 2019 STEPHEN L. MARTINEZ +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation +import Files + +struct CPInteractor { + + private let commitPrefixFile: File + private let commitPrefixModel: CommitPrefixModel + + init (gitDirectory: Folder) throws { + let (commitPrefixFile, commitPrefixModel) = try Self.build(using: gitDirectory) + self.commitPrefixFile = commitPrefixFile + self.commitPrefixModel = commitPrefixModel + } + + private static func build(using gitDirectory: Folder) throws -> (File, CommitPrefixModel) { + do { + let initialModelData = try JSONEncoder().encode(CommitPrefixModel.empty()) + let cpFile = try gitDirectory.createFileIfNeeded( + withName: FileName.commitPrefix, + contents: initialModelData) + let cpFileData = try cpFile.read() + let cpModel = try JSONDecoder().decode(CommitPrefixModel.self, from: cpFileData) + return (cpFile, cpModel) + } catch { + cpDebugPrint(error) + throw CPError.fileReadWriteError + } + } + + private func saveCommitPrefix(model: CommitPrefixModel) throws { + do { + let jsonEncoder = JSONEncoder() + let modelData = try jsonEncoder.encode(model) + try commitPrefixFile.write(modelData) + } catch { + cpDebugPrint(error) + throw CPError.fileReadWriteError + } + } + + private func branchPrefixes() throws -> [String] { + guard let regexValue = commitPrefixModel.regexValue else { + throw CPTermination.branchValidatorNotPresent + } + + let branch = Shell.currentBranch() ?? "" + let matches = branch.occurances(ofRegex: regexValue) + + guard matches.count > 0 else { + let validator = commitPrefixModel.branchValidator ?? "Validator Not Present" + throw CPTermination.invalidBranchPrefix(validator: validator) + } + + let uniqueMatches = Set(matches) + return Array(uniqueMatches) + } + + private func prefixFormatter(_ rawValue: String) -> [String] { + let parsedValues = rawValue + .split(separator: ",") + .map { String($0) } + + return parsedValues.map { "[\($0)]" } + } + + private func validatorFormatter(_ rawValue: String) throws -> String { + let validator = rawValue.trimmingCharacters(in: .whitespacesAndNewlines) + let containsNoNumbers = validator.occurances(ofRegex: #"(\d+)"#).isEmpty + let atLeastTwoCharacters = validator.count > 1 + guard containsNoNumbers && atLeastTwoCharacters else { + throw CPError.branchValidatorFormatError + } + return validator + } + + public func outputPrefixes() throws -> String { + switch commitPrefixModel.prefixMode { + case .normal: + return commitPrefixModel.prefixes.joined() + case .branchParse: + let retrievedBranchPrefixes = try branchPrefixes() + let branchPrefixes = retrievedBranchPrefixes.map { "[\($0)]" }.joined() + let normalPrefixes = commitPrefixModel.prefixes.joined() + return branchPrefixes + normalPrefixes + } + } + + public func getCommitPrefixState() throws -> CommitPrefixState { + switch commitPrefixModel.prefixMode { + case .normal: + return CommitPrefixState( + mode: .normal, + branchPrefixes: [], + normalPrefixes: commitPrefixModel.prefixes + ) + case .branchParse: + let retrievedBranchPrefixes = try branchPrefixes() + let branchPrefixes = retrievedBranchPrefixes.map { "[\($0)]" } + let normalPrefixes = commitPrefixModel.prefixes + return CommitPrefixState( + mode: .branchParse, + branchPrefixes: branchPrefixes, + normalPrefixes: normalPrefixes + ) + } + } + + public func deletePrefixes() throws -> String { + let newModel = commitPrefixModel.updated(with: []) + try saveCommitPrefix(model: newModel) + return "CommitPrefix DELETED" + } + + public func writeNew(prefixes rawValue: String) throws -> String { + let newPrefixes = prefixFormatter(rawValue) + let newModel = commitPrefixModel.updated(with: newPrefixes) + try saveCommitPrefix(model: newModel) + return "CommitPrefix STORED \(newPrefixes.joined())" + } + + public func activateBranchMode(with validator: String) throws -> String { + let formattedValidator = try validatorFormatter(validator) + let newModel = commitPrefixModel.updatedAsBranchMode(with: formattedValidator) + try saveCommitPrefix(model: newModel) + return "CommitPrefix MODE BRANCH_PARSE \(formattedValidator)" + } + + public func activateNormalMode() throws -> String { + switch commitPrefixModel.prefixMode { + case .normal: + return "CommitPrefix already in MODE NORMAL" + case .branchParse: + let newModel = commitPrefixModel.updatedAsNormalMode() + try saveCommitPrefix(model: newModel) + return "CommitPrefix MODE NORMAL" + } + } + +} diff --git a/Sources/CommitPrefix/CommitMessageHook.swift b/Sources/CommitPrefix/CommitMessageHook.swift index 41d88dc..d427459 100644 --- a/Sources/CommitPrefix/CommitMessageHook.swift +++ b/Sources/CommitPrefix/CommitMessageHook.swift @@ -29,7 +29,7 @@ import Foundation public struct CommitMessageHook { - private static let cpVersionNumber = "1.0.0" + private static let cpVersionNumber = "1.1.0" private let fileIdentifier = "Created by CommitPrefix \(Self.cpVersionNumber)" @@ -42,6 +42,11 @@ public struct CommitMessageHook { self.hooksDirectory = hooksDirectory } + public static func findOrCreate(with gitDirectory: Folder) throws { + let cpHook = try CommitMessageHook(gitDirectory: gitDirectory) + try cpHook.locateOrCreateHook() + } + private var currentDate: String { let formatter = DateFormatter() formatter.dateFormat = "MM/dd/yyyy" @@ -57,7 +62,7 @@ public struct CommitMessageHook { // import Foundation - + enum IOError: Error { case invalidArgument @@ -74,22 +79,36 @@ public struct CommitMessageHook { } - struct IOHelper { + struct IOCommitPrefix { let commitMsgPath: String - let prefixPath = ".git/CommitPrefix.txt" init(filePath: [String] = Array(CommandLine.arguments.dropFirst())) throws { - guard let firstArg = filePath.first else { - throw IOError.invalidArgument - } + guard let firstArg = filePath.first else { throw IOError.invalidArgument } self.commitMsgPath = firstArg } - func readContents(of filePath: String) -> String { + func getPrefixes() -> String { let readProcess = Process() readProcess.launchPath = "/usr/bin/env" - readProcess.arguments = ["cat", filePath] + readProcess.arguments = ["commitPrefix", "-o"] + + let pipe = Pipe() + readProcess.standardOutput = pipe + readProcess.launch() + + readProcess.waitUntilExit() + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + let contents = String(data: data, encoding: .utf8) + + return contents ?? "" + } + + func getCommitMessage() -> String { + let readProcess = Process() + readProcess.launchPath = "/usr/bin/env" + readProcess.arguments = ["cat", commitMsgPath] let pipe = Pipe() readProcess.standardOutput = pipe @@ -116,16 +135,16 @@ public struct CommitMessageHook { do { - let helper = try IOHelper() + let ioCommitPrefix = try IOCommitPrefix() - let commitMessage = helper.readContents(of: helper.commitMsgPath) + let prefixes = ioCommitPrefix.getPrefixes() .trimmingCharacters(in: .newlines) - let prefixMessage = helper.readContents(of: helper.prefixPath) + let commitMessage = ioCommitPrefix.getCommitMessage() .trimmingCharacters(in: .newlines) - let newCommitMessage = [prefixMessage, commitMessage].joined(separator: " ") - try helper.overwriteContents(with: newCommitMessage) + let newCommitMessage = [prefixes, commitMessage].joined(separator: " ") + try ioCommitPrefix.overwriteContents(with: newCommitMessage) } catch let ioError as IOError { @@ -136,19 +155,6 @@ public struct CommitMessageHook { """ } - private func makeExecutable(_ fileName: String) { - let executableProcess = Process() - executableProcess.launchPath = "/usr/bin/env" - cpDebugPrint(executableProcess.launchPath ?? "nil") - executableProcess.arguments = ["chmod", "755", fileName] - - let pipe = Pipe() - executableProcess.standardOutput = pipe - executableProcess.launch() - - executableProcess.waitUntilExit() - } - private func getCommitHookFile() throws -> File? { guard let foundCommitHookFile = try? hooksDirectory.file(named: FileName.commitMessage) else { @@ -157,7 +163,7 @@ public struct CommitMessageHook { let commitHookFile = try hooksDirectory.createFile(named: FileName.commitMessage) try commitHookFile.write(contents, encoding: .utf8) cpDebugPrint(commitHookFile.path) - makeExecutable(commitHookFile.path) + Shell.makeExecutable(commitHookFile.path) } catch { throw CPError.hookReadWriteError } @@ -188,12 +194,10 @@ public struct CommitMessageHook { } case "n": - print("Overwrite is cancelled") - exit(0) + throw CPTermination.overwriteCancelled default: - - throw CPError.expectedYesOrNo + throw CPTermination.expectedYesOrNo } } @@ -207,7 +211,7 @@ public struct CommitMessageHook { return hookContents.contains(fileIdentifier) } - public func locateOrCreateHook() throws { + private func locateOrCreateHook() throws { guard let foundCommitHookFile = try getCommitHookFile() else { return } guard try !hookIsCommitPrefix(foundCommitHookFile) else { return } try overwriteCommitHook(foundCommitHookFile) diff --git a/Sources/CommitPrefix/CommitPrefixModel.swift b/Sources/CommitPrefix/CommitPrefixModel.swift new file mode 100644 index 0000000..2e3327b --- /dev/null +++ b/Sources/CommitPrefix/CommitPrefixModel.swift @@ -0,0 +1,114 @@ +// +// CommitPrefixModel.swift +// commitPrefix +// +// MIT License +// +// Copyright (c) 2019 STEPHEN L. MARTINEZ +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation + +public enum PrefixMode: Int { + + case normal + case branchParse + +} + +public struct CommitPrefixModel: Codable { + + let prefixMode: PrefixMode + let branchValidator: String? + let prefixes: [String] + + /// Provides a regex if a branch validator is present + var regexValue: String? { + return branchValidator.flatMap { + return #"((?i)(\#($0))-(\d+))"# + } + } + + enum CodingKeys: String, CodingKey { + case prefixMode = "prefix_mode" + case branchValidator = "branch_issue_validator" + case prefixes = "prefixes" + } + + private init(prefixMode: PrefixMode, branchValidator: String?, prefixes: [String]) { + self.prefixMode = prefixMode + self.branchValidator = branchValidator + self.prefixes = prefixes + } + + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + let prefixModeRawValue = try values.decode(Int.self, forKey: .prefixMode) + self.prefixMode = PrefixMode(rawValue: prefixModeRawValue) ?? PrefixMode.normal + self.branchValidator = try values.decodeIfPresent(String.self, forKey: .branchValidator) + self.prefixes = try values.decode([String].self, forKey: .prefixes) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(prefixMode.rawValue, forKey: .prefixMode) + try container.encodeIfPresent(branchValidator, forKey: .branchValidator) + try container.encode(prefixes, forKey: .prefixes) + } + + static public func empty() -> CommitPrefixModel { + return CommitPrefixModel( + prefixMode: .normal, + branchValidator: nil, + prefixes: [] + ) + } + + public func updated(with newPrefixes: [String]) -> CommitPrefixModel { + return CommitPrefixModel( + prefixMode: prefixMode, + branchValidator: branchValidator, + prefixes: newPrefixes + ) + } + + public func updatedAsBranchMode(with newBranchValidator: String) -> CommitPrefixModel { + return CommitPrefixModel( + prefixMode: .branchParse, + branchValidator: newBranchValidator, + prefixes: prefixes + ) + } + + public func updatedAsNormalMode() -> CommitPrefixModel { + return CommitPrefixModel( + prefixMode: .normal, + branchValidator: nil, + prefixes: prefixes + ) + } + +} + +public struct CommitPrefixState { + let mode: PrefixMode + let branchPrefixes: [String] + let normalPrefixes: [String] +} diff --git a/Sources/CommitPrefix/Constants.swift b/Sources/CommitPrefix/Constants.swift index 89eaf2f..3dd5462 100644 --- a/Sources/CommitPrefix/Constants.swift +++ b/Sources/CommitPrefix/Constants.swift @@ -28,7 +28,7 @@ import Foundation public struct FileName { - public static let commitPrefix = "CommitPrefix.txt" + public static let commitPrefix = "CommitPrefix.JSON" public static let commitMessage = "commit-msg" } diff --git a/Sources/CommitPrefix/Shell.swift b/Sources/CommitPrefix/Shell.swift new file mode 100644 index 0000000..94b8497 --- /dev/null +++ b/Sources/CommitPrefix/Shell.swift @@ -0,0 +1,66 @@ +// +// Shell.swift +// commitPrefix +// +// MIT License +// +// Copyright (c) 2019 STEPHEN L. MARTINEZ +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation + +struct Shell { + + static func makeExecutable(_ fileName: String) { + let executableProcess = Process() + executableProcess.launchPath = "/usr/bin/env" + cpDebugPrint(executableProcess.launchPath ?? "nil") + executableProcess.arguments = ["chmod", "755", fileName] + + let pipe = Pipe() + executableProcess.standardOutput = pipe + executableProcess.launch() + + executableProcess.waitUntilExit() + } + + static func currentBranch() -> String? { + let gitProcess = Process() + gitProcess.launchPath = "/usr/bin/env" + gitProcess.arguments = ["git", "rev-parse", "--abbrev-ref", "HEAD"] + + let pipe = Pipe() + gitProcess.standardOutput = pipe + gitProcess.launch() + + gitProcess.waitUntilExit() + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + let branchName = String(data: data, encoding: .utf8) + let trimmedBranchName = branchName?.trimmingCharacters(in: .whitespacesAndNewlines) + + if trimmedBranchName == nil { + cpDebugPrint("Unable to get branch") + } + + return trimmedBranchName + } + +} diff --git a/Sources/CommitPrefix/String+Extensions.swift b/Sources/CommitPrefix/String+Extensions.swift new file mode 100644 index 0000000..9a4fac6 --- /dev/null +++ b/Sources/CommitPrefix/String+Extensions.swift @@ -0,0 +1,65 @@ +// +// String+Extensions.swift +// commitPrefix +// +// MIT License +// +// Copyright (c) 2019 STEPHEN L. MARTINEZ +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation + +public extension String { + + private func findMatches(in string: String, using regex: String) -> [String] { + + #if DEBUG + let isValid = (try? NSRegularExpression(pattern: regex, options: [])) != nil + assert(isValid, "Invalid Regex Pattern: \(regex)") + #endif + + var searchString = string + var foundMatches = [String]() + + var nextMatchFound: Range? { + searchString.range(of: regex, options: .regularExpression) + } + + func newSearch(string: String, removing range: Range) -> String { + var newString = string + let removingRange = string.startIndex.. [String] { + findMatches(in: self, using: pattern) + } + +} diff --git a/Sources/CommitPrefix/main.swift b/Sources/CommitPrefix/main.swift index 315cf87..532af76 100644 --- a/Sources/CommitPrefix/main.swift +++ b/Sources/CommitPrefix/main.swift @@ -24,27 +24,39 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -let commitPrefixCLI = CLIArguments() +import Foundation + +let cpCommandLineInterface = CLIArguments() do { let fileHandler = try CPFileHandler() - try fileHandler.locateOrCreateHook() - - switch try commitPrefixCLI.getCommand() { + switch try cpCommandLineInterface.getCommand() { - case .delete: - let deletedMessage = try fileHandler.deletePrefix() - print(deletedMessage) + case .viewState: + let currentState = try fileHandler.viewState() + print(currentState) - case .view: - let prefix = try fileHandler.outputPrefix() - print("CommitPrefix: \(prefix)") + case .outputPrefixes: + let prefixOutput = try fileHandler.outputPrefixes() + print(prefixOutput) + + case .deletePrefixes: + let deletionMessage = try fileHandler.deletePrefixes() + print(deletionMessage) + + case .modeNormal: + let modeSetMessage = try fileHandler.activateNormalMode() + print(modeSetMessage) - case .newEntry(entry: let entry): - let newPrefix = try fileHandler.writeNew(prefix: entry) - print("CommitPrefix saved: \(newPrefix)") + case .modeBranchParse(validator: let rawValidatorValue): + let modeSetMessage = try fileHandler.activateBranchMode(with: rawValidatorValue) + print(modeSetMessage) + + case .newPrefixes(value: let rawPrefixValue): + let storedPrefixesMessage = try fileHandler.writeNew(prefixes: rawPrefixValue) + print(storedPrefixesMessage) } @@ -52,5 +64,10 @@ do { print(prefixError.message) +} catch let terminationError as CPTermination { + + print(terminationError.message) + exit(0) + }