Last active
December 22, 2024 22:43
-
-
Save ddanier/4eaaeff5e5e2be62264ec23200d9a630 to your computer and use it in GitHub Desktop.
Conventional commit as `git cc` custom command for nu shell
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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] => [("src" | path join $sub.0), $sub.0], | |
$else => [$else.0, $else.0], | |
} | |
} | |
| filter { |$f| ($f.0 | path type) == "dir" } | |
| each { |$set| $set.1 } | |
| 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