Skip to content

Instantly share code, notes, and snippets.

@ddanier
Last active June 23, 2024 18:37
Show Gist options
  • Save ddanier/4eaaeff5e5e2be62264ec23200d9a630 to your computer and use it in GitHub Desktop.
Save ddanier/4eaaeff5e5e2be62264ec23200d9a630 to your computer and use it in GitHub Desktop.
Conventional commit as `git cc` custom command for nu shell
# This nu modules adds "git cc" to create commits following the conventional commit schema, see
# https://www.conventionalcommits.org/en/v1.0.0/
#
# Example usage:
# git cc -t feat -s my-feature -i my-issue -m "My feature"
# git cc fix -s my-fix -m "My fix"
#
# Is uses a slightly extended version of conventional commits to also include a commit type
# emoji and a commit issue. The commit scope will be inferred from the current git status,
# using changes folder names by default (using the "git cc-get-scope" command also provided
# by this module). The commit issue will be inferred from the current branch name, using
# the "git cc-get-issue" command also provided by this module. "git cc-get-issue" will detect
# Jira style issue branch names (like "dev/ABC-123-my-issue") and GitHub style issue branch
# names (like "dev/#123-my-issue").
#
# This module also provides some alias for quicker usage of the most common commit types, for
# example "git cc feat" will create a commit with the type "feat", just like "git commit -t feat"
# would.
#
# Note that the "git cc" command will override some of the default "git commit" options, like
# "-t", "-s" and "-i", etc. If you want to use those use the loger option names like "--template",
# "--signoff" and "--include" instead.
# Get conventional commit git scope for current changes (using src/ in monorepo schema)
export def "git cc-get-scope" [] {
git status --porcelain=v1
| lines
| split column -c " " state file
| where state != '??'
| get file
| each {
|file|
$file
| path split
| match $in {
['src', ..$sub] => $sub.0,
$else => $else.0,
}
} | uniq
}
# Get conventional commit git scope, but only if scope is unique (null otherwise)
export def "git cc-get-safe-scope" [] {
let scopes = git cc-get-scope
if ($scopes | length) != 1 {
return null
}
$scopes | first
}
# Get conventional commit git issue for current branch (using dev/... branch name schema)
export def "git cc-get-issue" [] {
let commit_branch: string = (git branch --show-current)
# Calculate ISSUE based on our branching schema
if ($commit_branch | str starts-with "dev/#") {
# Support GitHub style issues in dev-branches, like "dev/#123-some-issue"
try {
return ($commit_branch | parse --regex "^dev/(?P<issue>#[0-9]+)[_-]" | get issue | first)
}
} else if ($commit_branch | str starts-with "dev/") {
# Support Jira style issues in dev-branches, like "dev/ABC-123-some-issue"
try {
return ($commit_branch | parse --regex "^dev/(?P<issue>[a-zA-Z]+-[0-9]+)[_-]" | get issue | first)
}
}
}
# git commit using conventional commit schema
export def "git cc" [
--message (-m): string # Message of the commit
--commit-type (-t): string # Commit type (hint: use --template for original git commit -t)
--commit-scope (-s): string # Commit scope (hint: use --signoff for original git commit -s)
--commit-issue (-i): string # Commit issue (hint: use --include for original git commit -i)
--commit-breaking (-b) # Mark commit as breaking
--no-emoji # Don't use any emojis
# Default "git commit" arguments
# see https://github.com/nushell/nu_scripts/blob/main/custom-completions/git/git-completions.nu#L428
# ...but with the short variants of the options above removed (--template, --signoff, --include)
--all(-a) # automatically stage all modified and deleted files
--amend # amend the previous commit rather than adding a new one
--no-edit # don't edit the commit message (useful with --amend)
--reuse-message(-C): string # reuse the message from a previous commit
--reedit-message(-c): string # reuse and edit message from a commit
--fixup: string # create a fixup/amend commit
--squash: string # squash commit for autosquash rebase
--reset-author # reset author information
--short # short-format output for dry-run
--branch # show branch info in short-format
--porcelain # porcelain-ready format for dry-run
--long # long-format output for dry-run
--null(-z) # use NUL instead of LF in output
--file(-F): string # read commit message from file
--author: string # override commit author
--date: string # override author date
--template: string # use commit message template file
--signoff # add Signed-off-by trailer
--no-signoff # do not add Signed-off-by trailer
--trailer: string # add trailer to commit message
--no-verify(-n) # bypass pre-commit and commit-msg hooks
--verify # do not bypass pre-commit and commit-msg hooks
--allow-empty # allow commit with no changes
--allow-empty-message # allow commit with empty message
--cleanup: string # cleanup commit message
--edit(-e) # edit commit message
--no-edit # do not edit commit message
--include # include given paths in commit
--only(-o) # commit only specified paths
--pathspec-from-file: string # read pathspec from file
--pathspec-file-nul # use NUL character for pathspec file
--untracked-files(-u): string # show untracked files
--verbose(-v) # show diff in commit message template
--quiet(-q) # suppress commit summary
--dry-run # show paths to be committed without committing
--status # include git-status output in commit message
--no-status # do not include git-status output
--gpg-sign(-S):string # GPG-sign commit
--no-gpg-sign # do not GPG-sign commit
...pathspec: string # commit files matching pathspec
] {
let use_emoji = not $no_emoji
let emoji_map = {
"build": "πŸ› "
"chore": "♻️"
"ci": "βš™οΈ"
"docs": "πŸ“š"
"feat": "✨"
"fix": "πŸ›"
"perf": "πŸš€"
"refactor": "πŸ“¦"
"revert": "πŸ—‘"
"style": "πŸ’Ž"
"test": "🚨"
"release": "πŸ”–"
"sec": "πŸ”’"
"wip": "🚧"
}
# Ensure we have a type, ask if not
mut msg_commit_type = $commit_type
while ($msg_commit_type == null or $msg_commit_type == "") {
$msg_commit_type = (input "Enter the commit type (feat, fix, test, docs, chore, ...): ")
}
# Get scope from "git cc-get-safe-scope" if not provided
mut msg_commit_scope = $commit_scope
if ($msg_commit_scope == null) {
$msg_commit_scope = (git cc-get-safe-scope)
}
# Ensure we have a commit msg, ask if not
mut msg_message = $message
while ($msg_message == null or $msg_message == "") {
$msg_message = (input "Enter commit message: ")
}
# Get issue from "git cc-get-issue" if not provided
mut msg_commit_issue = $commit_issue
if ($msg_commit_issue == null) {
$msg_commit_issue = (git cc-get-issue)
}
# Prepare commit variables
let $msg_commit_issue = if ($msg_commit_issue != null and $msg_commit_issue != "") { $"($msg_commit_issue) " } else { "" }
let $msg_commit_scope = if ($msg_commit_scope != null and $msg_commit_scope != "") { $"\(($msg_commit_scope)\)" } else { "" }
let $msg_commit_breaking = if ($commit_breaking) { "!" } else { "" }
let $msg_emoji = if ($use_emoji and $msg_commit_type in $emoji_map) { $"($emoji_map | get $msg_commit_type) " } else { "" }
# Generate full commit msg
let $msg_full = ({
type: $msg_commit_type
scope: $msg_commit_scope
breaking: $msg_commit_breaking
issue: $msg_commit_issue
emoji: $msg_emoji
msg: $msg_message
} | format pattern "{type}{scope}{breaking}: {issue}{emoji}{msg}")
mut git_args = [
"--message",
$msg_full,
]
if $all { $git_args = ($git_args ++ "--all") }
if $amend { $git_args = ($git_args ++ "--amend") }
if $no_edit { $git_args = ($git_args ++ "--no-edit") }
if ($reuse_message != null) { $git_args = ($git_args ++ ["--reuse-message", $reuse_message]) }
if ($reedit_message != null) { $git_args = ($git_args ++ ["--reedit-message", $reedit_message]) }
if ($fixup != null) { $git_args = ($git_args ++ ["--fixup", $fixup]) }
if ($squash != null) { $git_args = ($git_args ++ ["--squash", $squash]) }
if $reset_author { $git_args = ($git_args ++ "--reset-author") }
if $short { $git_args = ($git_args ++ "--short") }
if $branch { $git_args = ($git_args ++ "--branch") }
if $porcelain { $git_args = ($git_args ++ "--porcelain") }
if $long { $git_args = ($git_args ++ "--long") }
if $null { $git_args = ($git_args ++ "--null") }
if ($file != null) { $git_args = ($git_args ++ ["--file", $file]) }
if ($author != null) { $git_args = ($git_args ++ ["--author", $author]) }
if ($date != null) { $git_args = ($git_args ++ ["--date", $date]) }
if ($template != null) { $git_args = ($git_args ++ ["--template", $template]) }
if $signoff { $git_args = ($git_args ++ "--signoff") }
if $no_signoff { $git_args = ($git_args ++ "--no-signoff") }
if ($trailer != null) { $git_args = ($git_args ++ ["--trailer", $trailer]) }
if $no_verify { $git_args = ($git_args ++ "--no-verify") }
if $verify { $git_args = ($git_args ++ "--verify") }
if $allow_empty { $git_args = ($git_args ++ "--allow-empty") }
if $allow_empty_message { $git_args = ($git_args ++ "--allow-empty-message") }
if ($cleanup != null) { $git_args = ($git_args ++ ["--cleanup", $cleanup]) }
if $edit { $git_args = ($git_args ++ "--edit") }
if $no_edit { $git_args = ($git_args ++ "--no-edit") }
if $amend { $git_args = ($git_args ++ "--amend") }
if $include { $git_args = ($git_args ++ "--include") }
if $only { $git_args = ($git_args ++ "--only") }
if ($pathspec_from_file != null) { $git_args = ($git_args ++ ["--pathspec-from-file", $pathspec_from_file]) }
if $pathspec_file_nul { $git_args = ($git_args ++ "--pathspec-file-nul") }
if ($untracked_files != null) { $git_args = ($git_args ++ ["--untracked-files", $untracked_files]) }
if $verbose { $git_args = ($git_args ++ "--verbose") }
if $quiet { $git_args = ($git_args ++ "--quiet") }
if $dry_run { $git_args = ($git_args ++ "--dry-run") }
if $status { $git_args = ($git_args ++ "--status") }
if $no_status { $git_args = ($git_args ++ "--no-status") }
if ($gpg_sign != null) { $git_args = ($git_args ++ ["--gpg-sign", $gpg_sign]) }
if $no_gpg_sign { $git_args = ($git_args ++ "--no-gpg-sign") }
if ($pathspec | is-not-empty) { $git_args = ($git_args ++ $pathspec) }
^git commit ...$git_args
}
export alias "git cc build" = git cc -t build
export alias "git cc chore" = git cc -t chore
export alias "git cc ci" = git cc -t ci
export alias "git cc docs" = git cc -t docs
export alias "git cc feat" = git cc -t feat
export alias "git cc fix" = git cc -t fix
export alias "git cc perf" = git cc -t perf
export alias "git cc refactor" = git cc -t refactor
export alias "git cc revert" = git cc -t revert
export alias "git cc style" = git cc -t style
export alias "git cc test" = git cc -t test
export alias "git cc release" = git cc -t release
export alias "git cc sec" = git cc -t sec
export alias "git cc wip" = git cc -t wip
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment