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
42 changes: 32 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 --
Expand Down Expand Up @@ -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.
186 changes: 139 additions & 47 deletions Sources/CommitPrefix/CLIArguments.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,108 +30,200 @@ 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<Bool>
private let view: OptionArgument<Bool>
private let newEntry: PositionalArgument<[String]>
private let outputPrefixes: OptionArgument<Bool>
private let deletePrefixes: OptionArgument<Bool>
private let modeNormal: OptionArgument<Bool>
private let modeBranchParse: OptionArgument<Bool>
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
}

}

private struct ArgumentBuilder {

let usage: String = """
<Commit Prefix Description>
[<PrefixValue1>,<PrefixValue2>,<PrefixValue3>...] [-o | --output] [-d | --delete]
[-n | -normal] [ -b | --branchParse <ValidatorValue> ]
"""

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 <PrefixValue1>,<PrefixValue2>,<PrefixValue3>...

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 <ValidatorValue>

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<Bool> {
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<Bool> {
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<Bool> {
func buildNormalArgument(parser: ArgumentParser) -> OptionArgument<Bool> {
return parser.add(
option: "--normal",
shortName: "-n",
kind: Bool.self,
usage: "Sets the mode to NORMAL",
completion: nil
)
}

func buildBranchParseArgument(parser: ArgumentParser) -> OptionArgument<Bool> {
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,
Expand Down
4 changes: 2 additions & 2 deletions Sources/CommitPrefix/CPDebugPrint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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("*****************************************")
}
33 changes: 30 additions & 3 deletions Sources/CommitPrefix/CPError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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:
Expand All @@ -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.
"""
}

}
Expand Down
Loading