Skip to content

Instantly share code, notes, and snippets.

@yehezkieldio
Created September 4, 2025 20:33
Show Gist options
  • Select an option

  • Save yehezkieldio/5701be56689ffd5b39218fcc397c2dd8 to your computer and use it in GitHub Desktop.

Select an option

Save yehezkieldio/5701be56689ffd5b39218fcc397c2dd8 to your computer and use it in GitHub Desktop.
#!/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