From dc3ee6130e137660f4954051a4b453f17165ca30 Mon Sep 17 00:00:00 2001 From: Ray Andrew Date: Thu, 4 Dec 2025 21:12:49 -0600 Subject: [PATCH] feat: add conventional commit --- bin/gc | 130 ++++++++++++++++++++++++++++++++++++++++++ bin/gc-changelog | 105 ++++++++++++++++++++++++++++++++++ bin/gc-check | 41 +++++++++++++ bin/presentation-mode | 2 +- bin/view-email-html | 36 +++++++++--- 5 files changed, 305 insertions(+), 9 deletions(-) create mode 100755 bin/gc create mode 100755 bin/gc-changelog create mode 100755 bin/gc-check diff --git a/bin/gc b/bin/gc new file mode 100755 index 0000000..70ab993 --- /dev/null +++ b/bin/gc @@ -0,0 +1,130 @@ +#!/usr/bin/env bash + +# Conventional commit helper +# Usage: gc [type] [message] [-s scope] [-b] [-B body] +# gc (interactive mode) + +set -o errexit +set -o nounset + +TYPES="feat fix docs style refactor test chore perf ci build" + +usage() { + echo "Usage: gc [type] [message] [-s scope] [-b] [-B body]" + echo " gc (interactive mode)" + echo "" + echo "Options:" + echo " -s, --scope Scope of the change" + echo " -b, --breaking Mark as breaking change (!)" + echo " -B, --body Commit body (longer description)" + echo "" + echo "Types: $TYPES" + echo "" + echo "Examples:" + echo " gc feat 'add user auth'" + echo " gc fix 'resolve crash' -s api # fix(api): resolve crash" + echo " gc feat 'new api' -b # feat!: new api" + echo " gc feat 'new api' -s auth -b # feat(auth)!: new api" + echo " gc feat 'big change' -B 'Details here'" + exit 1 +} + +build_message() { + local type="$1" + local scope="$2" + local breaking="$3" + local msg="$4" + local body="$5" + + local commit_msg="$type" + + if [ -n "$scope" ]; then + commit_msg+="($scope)" + fi + + if [ "$breaking" = "true" ]; then + commit_msg+="!" + fi + + commit_msg+=": $msg" + + if [ -n "$body" ]; then + commit_msg+=$'\n\n'"$body" + fi + + echo "$commit_msg" +} + +# Interactive mode if no args +if [ $# -eq 0 ]; then + echo "Types: $TYPES" + printf "Type: " + read -r type + printf "Scope (optional): " + read -r scope + printf "Breaking change? [y/N]: " + read -r breaking_input + printf "Message: " + read -r msg + printf "Body (optional, enter for none): " + read -r body + + breaking="false" + if [[ $breaking_input =~ ^[Yy] ]]; then + breaking="true" + fi + + commit_msg=$(build_message "$type" "$scope" "$breaking" "$msg" "$body") + git commit -m "$commit_msg" + exit 0 +fi + +# Parse arguments +type="" +msg="" +scope="" +breaking="false" +body="" + +while [ $# -gt 0 ]; do + case "$1" in + -s | --scope) + scope="$2" + shift 2 + ;; + -b | --breaking) + breaking="true" + shift + ;; + -B | --body) + body="$2" + shift 2 + ;; + -h | --help) + usage + ;; + *) + if [ -z "$type" ]; then + type="$1" + else + msg="$1" + fi + shift + ;; + esac +done + +# Validate type +if ! echo "$TYPES" | grep -qw "$type"; then + echo "Invalid type: $type" + echo "Valid types: $TYPES" + exit 1 +fi + +if [ -z "$msg" ]; then + echo "Message required" + usage +fi + +commit_msg=$(build_message "$type" "$scope" "$breaking" "$msg" "$body") +git commit -m "$commit_msg" diff --git a/bin/gc-changelog b/bin/gc-changelog new file mode 100755 index 0000000..0976b49 --- /dev/null +++ b/bin/gc-changelog @@ -0,0 +1,105 @@ +#!/usr/bin/env bash + +# Generate changelog from conventional commits +# Usage: gc-changelog [from_ref] [to_ref] +# gc-changelog # all commits +# gc-changelog v1.0.0 # from tag to HEAD +# gc-changelog v1.0.0 v2.0.0 # between tags + +set -o errexit +set -o nounset + +from_ref=${1:-} +to_ref=${2:-HEAD} + +if [ -n "$from_ref" ]; then + range="$from_ref..$to_ref" +else + range="" +fi + +# Collect commits by type +declare -A titles=( + [feat]="Features" + [fix]="Bug Fixes" + [docs]="Documentation" + [style]="Styles" + [refactor]="Refactoring" + [test]="Tests" + [chore]="Chores" + [perf]="Performance" + [ci]="CI" + [build]="Build" +) + +declare -A commits +declare -a breaking_changes + +for type in feat fix docs style refactor test chore perf ci build; do + commits[$type]="" +done + +# Parse commits +# Full pattern: type(scope)!: description +while IFS= read -r line; do + [ -z "$line" ] && continue + + msg=$(echo "$line" | cut -d' ' -f2-) + + for type in feat fix docs style refactor test chore perf ci build; do + # Match: type, optional (scope), optional !, colon, space + if echo "$msg" | grep -qE "^$type(\([^)]+\))?!?: "; then + is_breaking=false + if echo "$msg" | grep -qE "^$type(\([^)]+\))?!:"; then + is_breaking=true + fi + + # Extract scope and description + if echo "$msg" | grep -qE "^$type\([^)]+\)"; then + scope=$(echo "$msg" | sed -E "s/^$type\(([^)]+)\)!?: .*/\1/") + desc=$(echo "$msg" | sed -E "s/^$type\([^)]+\)!?: //") + else + scope="" + desc=$(echo "$msg" | sed -E "s/^$type!?: //") + fi + + # Format the entry + if [ -n "$scope" ]; then + entry="- **$scope**: $desc" + else + entry="- $desc" + fi + + if [ "$is_breaking" = true ]; then + breaking_changes+=("$entry") + fi + + commits[$type]+="$entry"$'\n' + break + fi + done +done < <(git log --oneline $range) + +# Output changelog +echo "# Changelog" +echo "" + +# Breaking changes first +if [ ${#breaking_changes[@]} -gt 0 ]; then + echo "## BREAKING CHANGES" + echo "" + for change in "${breaking_changes[@]}"; do + echo "$change" + done + echo "" +fi + +# Then by type +for type in feat fix perf refactor docs style test chore ci build; do + if [ -n "${commits[$type]}" ]; then + echo "## ${titles[$type]}" + echo "" + echo -n "${commits[$type]}" + echo "" + fi +done diff --git a/bin/gc-check b/bin/gc-check new file mode 100755 index 0000000..23c2845 --- /dev/null +++ b/bin/gc-check @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +# Validate conventional commit messages in git history +# Usage: gc-check [number_of_commits] + +set -o errexit +set -o nounset + +# Full conventional commit regex: +# type(optional-scope)optional-!: description +COMMIT_REGEX='^(feat|fix|docs|style|refactor|test|chore|perf|ci|build)(\([^)]+\))?!?: .+' + +count=${1:-10} +errors=0 + +echo "Checking last $count commits..." +echo "" + +while IFS= read -r line; do + hash=$(echo "$line" | cut -d' ' -f1) + msg=$(echo "$line" | cut -d' ' -f2-) + + if echo "$msg" | grep -qE "$COMMIT_REGEX"; then + if echo "$msg" | grep -qE '^[^:]+!:'; then + echo "✓ $hash $msg (BREAKING)" + else + echo "✓ $hash $msg" + fi + else + echo "✗ $hash $msg" + errors=$((errors + 1)) + fi +done < <(git log --oneline -n "$count") + +echo "" +if [ $errors -gt 0 ]; then + echo "$errors invalid commit(s) found" + exit 1 +else + echo "All commits valid" +fi diff --git a/bin/presentation-mode b/bin/presentation-mode index 66c95f2..4d3700f 100755 --- a/bin/presentation-mode +++ b/bin/presentation-mode @@ -40,7 +40,7 @@ elif [ "$1" == "off" ]; then done <"$BACKUP_FILE" # Restore original wallpaper - osascript -e 'tell application "System Events" to set picture of every desktop to "/Users/rayandrew/Pictures/Wallpapers/bluering.png"' >/dev/null 2>&1 + osascript -e 'tell application "System Events" to set picture of every desktop to "/Users/rayandrew/Pictures/Wallpapers/420322.jpg"' >/dev/null 2>&1 rm "$BACKUP_FILE" aerospace reload-config diff --git a/bin/view-email-html b/bin/view-email-html index a99f2ad..6ae2481 100755 --- a/bin/view-email-html +++ b/bin/view-email-html @@ -34,7 +34,8 @@ else echo "body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 30px 20px; max-width: 800px; margin: 0 auto; background: var(--bg); color: var(--text); transition: all 0.2s ease; line-height: 1.5; }" echo "pre { white-space: pre-wrap; word-wrap: break-word; }" echo ".headers { background: var(--header-bg); padding: 20px 25px; margin-bottom: 25px; border-radius: 12px; border: 1px solid var(--border); box-shadow: 0 2px 8px var(--shadow); }" - echo ".headers p { margin: 8px 0; display: grid; grid-template-columns: 70px 1fr; gap: 8px; align-items: baseline; }" + echo ".headers p { margin: 8px 0; display: grid; grid-template-columns: 80px 1fr; gap: 8px; align-items: baseline; }" + echo ".headers strong { white-space: nowrap; }" echo ".headers strong { color: var(--muted); font-weight: 500; }" echo ".headers .addr-list { display: flex; flex-wrap: wrap; gap: 6px; }" echo ".headers .addr { padding: 4px 10px; background: var(--bg); border-radius: 6px; font-size: 0.95em; }" @@ -53,7 +54,10 @@ else echo "a { color: var(--link); text-decoration: none; } a:hover { text-decoration: underline; }" echo ".toggle { position: fixed; top: 20px; right: 20px; width: 44px; height: 44px; border: 1px solid var(--border); border-radius: 50%; cursor: pointer; background: var(--header-bg); color: var(--text); font-size: 20px; transition: all 0.2s; box-shadow: 0 2px 8px var(--shadow); }" echo ".toggle:hover { transform: scale(1.05); box-shadow: 0 4px 12px var(--shadow); }" - echo ".sun { display: none; } .dark .sun { display: inline; } .dark .moon { display: none; }" + echo ".toggle .icon { display: none; }" + echo ".toggle.auto .icon-auto { display: inline; }" + echo ".toggle.light .icon-light { display: inline; }" + echo ".toggle.dark .icon-dark { display: inline; }" echo ".attachments { margin-top: 25px; border: 1px solid var(--border); border-radius: 12px; overflow: hidden; }" echo ".attachments-header { padding: 12px 20px; background: var(--header-bg); cursor: pointer; font-weight: 500; color: var(--muted); }" echo ".attachments-header:hover { background: var(--border); }" @@ -72,9 +76,14 @@ else echo ".lightbox-close:hover { background: rgba(0,0,0,0.8); }" echo "" echo '' - echo '' + echo '' echo "