Created
September 4, 2025 20:33
-
-
Save yehezkieldio/5701be56689ffd5b39218fcc397c2dd8 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
| #!/bin/bash | |
| readonly HASH="%C(always,yellow)%h%C(always,reset)" | |
| readonly RELATIVE_TIME="%C(always,green)%ar%C(always,reset)" | |
| readonly AUTHOR="%C(always,bold blue)%an%C(always,reset)" | |
| readonly REFS="%C(always,red)%d%C(always,reset)" | |
| readonly SUBJECT="%s" | |
| readonly FORMAT="$HASH $RELATIVE_TIME{$AUTHOR{$REFS $SUBJECT" | |
| readonly FORMAT_WITHOUT_AUTHOR="{$REFS $SUBJECT" | |
| _check_git_repo() { | |
| if ! git rev-parse --git-dir >/dev/null 2>&1; then | |
| echo "Error: Not in a git repository" >&2 | |
| return 1 | |
| fi | |
| } | |
| _check_command() { | |
| local cmd="$1" | |
| local context="${2:-this operation}" | |
| if ! command -v "$cmd" >/dev/null 2>&1; then | |
| echo "Error: '$cmd' command not found, required for $context" >&2 | |
| return 1 | |
| fi | |
| } | |
| _safe_less() { | |
| if [[ -t 1 ]]; then | |
| less -XRS --quit-if-one-screen | |
| else | |
| cat | |
| fi | |
| } | |
| pretty_git_log() { | |
| _check_git_repo || return 1 | |
| local show_author=true | |
| local max_count="" | |
| local branch_args=() | |
| # Parse options | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| --no-author) | |
| show_author=false | |
| shift | |
| ;; | |
| -n|--max-count) | |
| if [[ -n "$2" && "$2" =~ ^[0-9]+$ ]]; then | |
| max_count="--max-count=$2" | |
| shift 2 | |
| else | |
| echo "Error: --max-count requires a positive number" >&2 | |
| return 1 | |
| fi | |
| ;; | |
| -a|--all) | |
| branch_args=(--all) | |
| shift | |
| ;; | |
| -h|--help) | |
| cat << 'EOF' | |
| Usage: pretty_git_log [options] [git-log-args...] | |
| Pretty formatted git log with colors and columns. | |
| Options: | |
| --no-author Hide author information | |
| -n, --max-count N Limit to N commits | |
| -a, --all Show all branches | |
| -h, --help Show this help | |
| Examples: | |
| pretty_git_log # Show recent commits | |
| pretty_git_log --no-author # Hide author column | |
| pretty_git_log -n 10 --all # Show 10 commits from all branches | |
| EOF | |
| return 0 | |
| ;; | |
| --) | |
| shift | |
| break | |
| ;; | |
| *) | |
| # Pass through other git log arguments | |
| branch_args+=("$1") | |
| shift | |
| ;; | |
| esac | |
| done | |
| # Add remaining arguments | |
| branch_args+=("$@") | |
| local format_to_use="$FORMAT" | |
| if [[ "$show_author" == false ]]; then | |
| format_to_use="$HASH $RELATIVE_TIME$FORMAT_WITHOUT_AUTHOR" | |
| fi | |
| # Build git command | |
| local -a git_args=(log --graph --pretty="tformat:$format_to_use") | |
| [[ -n "$max_count" ]] && git_args+=("$max_count") | |
| git_args+=("${branch_args[@]}") | |
| git "${git_args[@]}" | column -t -s '{' | _safe_less | |
| } | |
| pretty_git_log_without_author() { | |
| pretty_git_log --no-author "$@" | |
| } | |
| pretty_git_tree() { | |
| _check_git_repo || return 1 | |
| local max_count="" | |
| local branch_args=(--all) | |
| # Parse options | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| -n|--max-count) | |
| if [[ -n "$2" && "$2" =~ ^[0-9]+$ ]]; then | |
| max_count="--max-count=$2" | |
| shift 2 | |
| else | |
| echo "Error: --max-count requires a positive number" >&2 | |
| return 1 | |
| fi | |
| ;; | |
| --current-branch) | |
| branch_args=() | |
| shift | |
| ;; | |
| -h|--help) | |
| cat << 'EOF' | |
| Usage: pretty_git_tree [options] | |
| Show a tree view of git history. | |
| Options: | |
| -n, --max-count N Limit to N commits | |
| --current-branch Show only current branch | |
| -h, --help Show this help | |
| Examples: | |
| pretty_git_tree # Show all branches | |
| pretty_git_tree -n 20 # Limit to 20 commits | |
| pretty_git_tree --current-branch # Only current branch | |
| EOF | |
| return 0 | |
| ;; | |
| *) | |
| echo "Error: Unknown option '$1'" >&2 | |
| return 1 | |
| ;; | |
| esac | |
| done | |
| local -a git_args=(log --oneline --graph --decorate) | |
| [[ -n "$max_count" ]] && git_args+=("$max_count") | |
| git_args+=("${branch_args[@]}") | |
| git "${git_args[@]}" | _safe_less | |
| } | |
| untagged_commits() { | |
| _check_git_repo || return 1 | |
| local format="* %s" | |
| local show_hash=false | |
| local show_author=false | |
| # Parse options | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| --hash) | |
| show_hash=true | |
| shift | |
| ;; | |
| --author) | |
| show_author=true | |
| shift | |
| ;; | |
| -h|--help) | |
| cat << 'EOF' | |
| Usage: untagged_commits [options] | |
| Show commits since the last tag. | |
| Options: | |
| --hash Include commit hash | |
| --author Include author information | |
| -h, --help Show this help | |
| Examples: | |
| untagged_commits # Simple list of commit messages | |
| untagged_commits --hash # Include commit hashes | |
| EOF | |
| return 0 | |
| ;; | |
| *) | |
| echo "Error: Unknown option '$1'" >&2 | |
| return 1 | |
| ;; | |
| esac | |
| done | |
| # Build format string | |
| if [[ "$show_hash" == true ]]; then | |
| format="* %h" | |
| [[ "$show_author" == true ]] && format="$format (%an)" | |
| format="$format %s" | |
| elif [[ "$show_author" == true ]]; then | |
| format="* (%an) %s" | |
| fi | |
| local latest_tag | |
| latest_tag=$(git describe --tags --abbrev=0 2>/dev/null) | |
| if [[ -z "$latest_tag" ]]; then | |
| echo "No tags found, showing all commits:" >&2 | |
| git log --pretty=format:"$format" | |
| else | |
| echo "Commits since tag '$latest_tag':" >&2 | |
| git log "$latest_tag"..HEAD --pretty=format:"$format" | |
| fi | |
| } | |
| diffone() { | |
| _check_git_repo || return 1 | |
| local auto_advance=false | |
| local show_stats=false | |
| # Parse options | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| -y|--auto) | |
| auto_advance=true | |
| shift | |
| ;; | |
| -s|--stats) | |
| show_stats=true | |
| shift | |
| ;; | |
| -h|--help) | |
| cat << 'EOF' | |
| Usage: diffone [options] | |
| View staged changes one file at a time. | |
| Options: | |
| -y, --auto Auto-advance without prompting | |
| -s, --stats Show diff statistics | |
| -h, --help Show this help | |
| Controls (when not using --auto): | |
| Enter: Next file | |
| q: Quit | |
| s: Show stats for current file | |
| EOF | |
| return 0 | |
| ;; | |
| *) | |
| echo "Error: Unknown option '$1'" >&2 | |
| return 1 | |
| ;; | |
| esac | |
| done | |
| local files | |
| files=$(git diff --name-only --cached) | |
| if [[ -z "$files" ]]; then | |
| echo "No staged changes to show" | |
| return 0 | |
| fi | |
| local -a file_array | |
| readarray -t file_array <<< "$files" | |
| local total=${#file_array[@]} | |
| echo "Found $total staged file(s)" | |
| [[ "$show_stats" == true ]] && git diff --cached --stat | |
| for ((i=0; i<total; i++)); do | |
| local file="${file_array[i]}" | |
| local file_num=$((i+1)) | |
| echo -e "\n\033[1;36m=== File $file_num/$total: $file ===\033[0m" | |
| if [[ "$show_stats" == true ]]; then | |
| git diff --cached --stat "$file" | |
| echo | |
| fi | |
| git diff --cached --color "$file" | |
| if [[ "$auto_advance" == false && $((i+1)) -lt total ]]; then | |
| echo -e "\n\033[1;33mControls: [Enter] Next, [q] Quit, [s] Stats\033[0m" | |
| read -r -p "> " response | |
| case "$response" in | |
| q|Q) | |
| echo "Exiting..." | |
| break | |
| ;; | |
| s|S) | |
| git diff --cached --stat "$file" | |
| echo -e "\nPress Enter to continue..." | |
| read -r | |
| ;; | |
| esac | |
| fi | |
| done | |
| } | |
| search_commits() { | |
| _check_git_repo || return 1 | |
| local query="" | |
| local author="" | |
| local branch_args=(--all) | |
| local since="" | |
| local until="" | |
| local copy_hash=false | |
| local use_fzf=false | |
| local max_count="" | |
| local format="--oneline" | |
| local case_sensitive=false | |
| local files_only=false | |
| local show_merges=true | |
| local show_stats=false | |
| # Parse arguments | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| -q|--query) | |
| if [[ -z "$2" ]]; then | |
| echo "Error: --query requires a value" >&2 | |
| return 1 | |
| fi | |
| query="$2" | |
| shift 2 | |
| ;; | |
| -a|--author) | |
| if [[ -z "$2" ]]; then | |
| echo "Error: --author requires a value" >&2 | |
| return 1 | |
| fi | |
| author="$2" | |
| shift 2 | |
| ;; | |
| -b|--branch) | |
| if [[ -z "$2" ]]; then | |
| echo "Error: --branch requires a value" >&2 | |
| return 1 | |
| fi | |
| branch_args=("$2") | |
| shift 2 | |
| ;; | |
| --since) | |
| if [[ -z "$2" ]]; then | |
| echo "Error: --since requires a value" >&2 | |
| return 1 | |
| fi | |
| since="$2" | |
| shift 2 | |
| ;; | |
| --until) | |
| if [[ -z "$2" ]]; then | |
| echo "Error: --until requires a value" >&2 | |
| return 1 | |
| fi | |
| until="$2" | |
| shift 2 | |
| ;; | |
| -n|--max-count) | |
| if [[ -z "$2" || ! "$2" =~ ^[0-9]+$ ]]; then | |
| echo "Error: --max-count requires a positive number" >&2 | |
| return 1 | |
| fi | |
| max_count="$2" | |
| shift 2 | |
| ;; | |
| --format) | |
| if [[ -z "$2" ]]; then | |
| echo "Error: --format requires a value" >&2 | |
| return 1 | |
| fi | |
| format="$2" | |
| shift 2 | |
| ;; | |
| -c|--copy) | |
| copy_hash=true | |
| shift | |
| ;; | |
| -f|--fzf) | |
| use_fzf=true | |
| shift | |
| ;; | |
| -i|--case-sensitive) | |
| case_sensitive=true | |
| shift | |
| ;; | |
| --files-only) | |
| files_only=true | |
| shift | |
| ;; | |
| --no-merges) | |
| show_merges=false | |
| shift | |
| ;; | |
| --stats) | |
| show_stats=true | |
| shift | |
| ;; | |
| -h|--help) | |
| cat << 'EOF' | |
| Usage: search_commits [options] [query] | |
| Search and filter git commits with various options. | |
| Options: | |
| -q, --query <text> Search text in commit messages and content | |
| -a, --author <name> Filter by author name or email | |
| -b, --branch <ref> Search in specific branch/ref (default: --all) | |
| --since <date> Filter commits since date (e.g., "2 weeks ago") | |
| --until <date> Filter commits until date | |
| -n, --max-count <num> Limit number of commits shown | |
| --format <format> Git log format (default: oneline) | |
| -c, --copy Copy selected commit hash to clipboard | |
| -f, --fzf Use fzf for interactive selection | |
| -i, --case-sensitive Case-sensitive search | |
| --files-only Search only in changed file names | |
| --no-merges Exclude merge commits | |
| --stats Show diff statistics | |
| -h, --help Show this help message | |
| Examples: | |
| search_commits "fix bug" # Search for "fix bug" | |
| search_commits -a "john" -f # Interactive search for John's commits | |
| search_commits -b main --since "1 week ago" # Recent commits on main | |
| search_commits -q "refactor" -c -f # Search and copy hash interactively | |
| search_commits --files-only "config" # Search in file names only | |
| EOF | |
| return 0 | |
| ;; | |
| --) | |
| shift | |
| query="$*" | |
| break | |
| ;; | |
| -*) | |
| echo "Error: Unknown option '$1'" >&2 | |
| return 1 | |
| ;; | |
| *) | |
| [[ -z "$query" ]] && query="$1" | |
| shift | |
| ;; | |
| esac | |
| done | |
| # Validate fzf availability if needed | |
| if [[ "$use_fzf" == true ]]; then | |
| _check_command fzf "interactive selection" || return 1 | |
| fi | |
| # Build git log command | |
| local -a log_args=(log) | |
| # Add format | |
| log_args+=("$format") | |
| # Add branch arguments | |
| log_args+=("${branch_args[@]}") | |
| # Add filters | |
| [[ -n "$since" ]] && log_args+=("--since=$since") | |
| [[ -n "$until" ]] && log_args+=("--until=$until") | |
| [[ -n "$max_count" ]] && log_args+=("--max-count=$max_count") | |
| [[ -n "$author" ]] && log_args+=("--author=$author") | |
| [[ "$show_merges" == false ]] && log_args+=(--no-merges) | |
| [[ "$show_stats" == true ]] && log_args+=(--stat) | |
| # Handle search execution | |
| if [[ "$use_fzf" == true ]]; then | |
| _search_commits_fzf "$query" "$copy_hash" "$case_sensitive" "$files_only" "${log_args[@]}" | |
| else | |
| _search_commits_simple "$query" "$case_sensitive" "$files_only" "${log_args[@]}" | |
| fi | |
| } | |
| _search_commits_fzf() { | |
| local query="$1" | |
| local copy_hash="$2" | |
| local case_sensitive="$3" | |
| local files_only="$4" | |
| shift 4 | |
| local -a log_args=("$@") | |
| # Execute git command and get output | |
| local git_output | |
| git_output=$(git "${log_args[@]}") | |
| # Apply text filtering if query is provided | |
| if [[ -n "$query" ]]; then | |
| local -a grep_opts=(-i) | |
| [[ "$case_sensitive" == true ]] && grep_opts=() | |
| if [[ "$files_only" == true ]]; then | |
| # For files-only search, we need to search in file names | |
| git_output=$(echo "$git_output" | while read -r line; do | |
| local hash=$(echo "$line" | grep -o '[a-f0-9]\{7,\}' | head -n1) | |
| if [[ -n "$hash" ]]; then | |
| local files | |
| files=$(git show --name-only --format= "$hash") | |
| if echo "$files" | grep "${grep_opts[@]}" -q "$query"; then | |
| echo "$line" | |
| fi | |
| fi | |
| done) | |
| else | |
| # Regular search in commit messages and content | |
| if command -v rg >/dev/null 2>&1; then | |
| git_output=$(echo "$git_output" | rg "${grep_opts[@]}" "$query") | |
| else | |
| git_output=$(echo "$git_output" | grep "${grep_opts[@]}" "$query") | |
| fi | |
| fi | |
| fi | |
| # Check if we have any results | |
| if [[ -z "$git_output" ]]; then | |
| echo "No commits found matching criteria" | |
| return 0 | |
| fi | |
| # Use fzf for selection | |
| local selected | |
| selected=$(echo "$git_output" | fzf --ansi \ | |
| --preview 'echo {} | grep -o "[a-f0-9]\{7,\}" | head -n1 | xargs -I {} git show --color=always {}' \ | |
| --preview-window=right:60%:wrap \ | |
| --bind="ctrl-d:preview-page-down,ctrl-u:preview-page-up" \ | |
| --bind="ctrl-s:execute(echo {} | grep -o \"[a-f0-9]\\{7,\\}\" | head -n1 | xargs -I {} git show --stat {})" \ | |
| --header="Enter: show commit, Ctrl-S: stats, Ctrl-C: exit, Ctrl-D/U: scroll preview") | |
| [[ -z "$selected" ]] && return 0 | |
| # Extract commit hash | |
| local commit_hash | |
| commit_hash=$(echo "$selected" | grep -o '[a-f0-9]\{7,\}' | head -n1) | |
| if [[ -z "$commit_hash" ]]; then | |
| echo "Error: Could not extract commit hash from selection" >&2 | |
| return 1 | |
| fi | |
| if [[ "$copy_hash" == true ]]; then | |
| _copy_to_clipboard "$commit_hash" | |
| else | |
| git show "$commit_hash" | |
| fi | |
| } | |
| _search_commits_simple() { | |
| local query="$1" | |
| local case_sensitive="$2" | |
| local files_only="$3" | |
| shift 3 | |
| local -a log_args=("$@") | |
| # Execute git command | |
| local git_output | |
| git_output=$(git "${log_args[@]}") | |
| # Apply filtering if query is provided | |
| if [[ -n "$query" ]]; then | |
| local -a grep_opts=(-i --color=always) | |
| [[ "$case_sensitive" == true ]] && grep_opts=(--color=always) | |
| if [[ "$files_only" == true ]]; then | |
| # For files-only search | |
| git_output=$(echo "$git_output" | while read -r line; do | |
| local hash=$(echo "$line" | grep -o '[a-f0-9]\{7,\}' | head -n1) | |
| if [[ -n "$hash" ]]; then | |
| local files | |
| files=$(git show --name-only --format= "$hash") | |
| if echo "$files" | grep "${grep_opts[@]:0:1}" -q "$query"; then | |
| echo "$line" | |
| fi | |
| fi | |
| done) | |
| else | |
| # Regular search | |
| if command -v rg >/dev/null 2>&1; then | |
| git_output=$(echo "$git_output" | rg "${grep_opts[@]}" "$query") | |
| else | |
| git_output=$(echo "$git_output" | grep "${grep_opts[@]}" "$query") | |
| fi | |
| fi | |
| fi | |
| # Output results | |
| if [[ -z "$git_output" ]]; then | |
| echo "No commits found matching criteria" | |
| return 0 | |
| fi | |
| echo "$git_output" | _safe_less | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment