Skip to content

Instantly share code, notes, and snippets.

@emaballarin
Last active August 4, 2025 00:01
Show Gist options
  • Save emaballarin/d238ef3724110d0cbd9577dde64eb176 to your computer and use it in GitHub Desktop.
Save emaballarin/d238ef3724110d0cbd9577dde64eb176 to your computer and use it in GitHub Desktop.
Tool for AI-powered code review, wrapping around git diff
#!/usr/bin/env bash
# An original project of Bill Mill (https://billmill.org/):
# See: https://notes.billmill.org/blog/2025/07/An_AI_tool_I_find_useful.html
# https://github.com/llimllib/personal_code/blob/master/homedir/.local/bin/review
# LICENSE: unlicense. This is free and unencumbered software released into the public domain.
# see unlicense.org for full license
set -euo pipefail
# Configuration
MAX_TOKENS=${AIREVIEW_MAX_TOKENS:-50000}
DEFAULT_CONTEXT=${AIREVIEW_DEFAULT_CONTEXT:-10}
function usage {
cat <<EOF
aireview [--verbose] [--context TEXT|-] [--help] [git-diff-arguments...]
Ask an LLM to review code changes. This tool passes arguments directly to 'git diff',
allowing you to use any git diff syntax or options.
Options:
-c, --context TEXT Add additional context for the review, appended to the system prompt. Will be concatenated if provided multiple times
--context - Read additional context from stdin
-h, --help Show this help message
-v, --verbose Enable verbose output
Review Examples:
# Review unstaged changes
aireview
# Review with additional context
aireview --context "Focus your review on possible authentication bypasses"
# Review with context from stdin
cat PR_DESCRIPTION.md | aireview --context -
# Review with context from a command
git log -1 --pretty=%B | aireview --context -
# Review staged changes
aireview --cached
# Review changes between HEAD and main
aireview main
# Review changes between two branches
aireview main feature-branch
# OR
aireview main..feature-branch
# Review only changes since branch diverged from main
aireview main...feature-branch
# Review a remote branch
aireview origin/main..origin/feature-branch
# Limit review to specific files
aireview main -- src/components/
# Adjust context lines
aireview -U5 main
Dot Notation:
- Two dots (A..B): Direct comparison between A and B
- Three dots (A...B): Compare common ancestor of A and B with B
Depends on:
- llm: https://github.com/simonw/llm
- bat: https://github.com/sharkdp/bat (optional)
EOF
exit "${1:-0}"
}
git_args=()
has_unified_context=false
context_value=$DEFAULT_CONTEXT
additional_context=""
readonly RED='\033[0;31m'
readonly BLUE='\033[0;34m'
readonly RESET='\033[0m'
info() {
printf "${BLUE}• %s${RESET}\n" "$1" >&2
}
error() {
printf "${RED}❌ %s${RESET}\n" "$1" >&2
exit 1
}
usage_error() {
printf "${RED}❌ %s${RESET}\n" "$1" >&2
usage 1
}
estimate_tokens() {
local content="$1"
local chars=${#content}
# Code tends to have more tokens per character than prose
local chars_per_token=3
echo $((chars / chars_per_token))
}
find_optimal_context() {
local min_context=1
local max_context="$context_value"
local optimal_context=$min_context
while [[ $min_context -le $max_context ]]; do
local test_context=$(( (min_context + max_context) / 2 ))
# Replace unified context in git args for testing
local test_git_args=()
for arg in "${git_args[@]}"; do
if [[ "$arg" =~ ^-U[0-9]+$ ]]; then
test_git_args+=("-U$test_context")
elif [[ "$arg" =~ ^--unified=[0-9]+$ ]]; then
test_git_args+=("--unified=$test_context")
else
test_git_args+=("$arg")
fi
done
# Test this context level
local test_diff_output
test_diff_output=$(git diff "${test_git_args[@]}" 2>/dev/null || error "Git diff command failed during context optimization")
local test_tokens
test_tokens=$(estimate_tokens "$test_diff_output")
if [[ $test_tokens -le $MAX_TOKENS ]]; then
optimal_context=$test_context
min_context=$((test_context + 1))
else
max_context=$((test_context - 1))
fi
done
echo $optimal_context
}
# Process only our custom arguments, pass everything else to git
while [[ $# -gt 0 ]]; do
case "$1" in
-v|--verbose)
set -x
shift
;;
-c|--context)
shift
if [[ $# -gt 0 ]]; then
# Read from stdin for context
if [[ "$1" == "-" ]]; then
if [[ -t 0 ]]; then
usage_error "No stdin input available for --context -"
fi
new_context=$(cat)
if [[ -z "$new_context" ]]; then
usage_error "Empty input from stdin for --context -"
fi
else
new_context=$1
fi
if [[ -n "$additional_context" ]]; then
additional_context="${additional_context}
$new_context"
else
additional_context="$new_context"
fi
shift
else
usage_error "Missing value for --context option"
fi
;;
-U[0-9]*)
# Form: -U10
has_unified_context=true
context_value="${1#-U}"
git_args+=("$1")
shift
;;
-U)
# Form: -U 10
has_unified_context=true
shift
if [[ $# -gt 0 && "$1" =~ ^[0-9]+$ ]]; then
context_value="$1"
# normalize to `-U10` to ease our checking later on
git_args+=("-U$1")
shift
else
usage_error "Missing value for -U option"
fi
;;
--unified=*)
# Form: --unified=10
has_unified_context=true
context_value="${1#--unified=}"
git_args+=("$1")
shift
;;
-h|--help)
usage
;;
*)
# Store all other arguments to pass to git diff
git_args+=("$1")
shift
;;
esac
done
if ! command -v llm >/dev/null 2>&1; then
error "Missing required command llm. On mac: brew install llm"
fi
# Default unified context if none specified. The idea here is to increase the
# context (git defaults to 3 lines) so that the LLM has more context for its
# review. Later on we'll check if this generates too much output and shorten it
# if so
if [[ "$has_unified_context" == false ]]; then
git_args=("-U$context_value" "${git_args[@]}")
fi
# Run git diff
if [[ ${#git_args[@]} -gt 0 ]]; then
diff_output=$(git diff "${git_args[@]}" 2>/dev/null || error "Git diff command failed. Check your arguments.")
else
diff_output=$(git diff 2>/dev/null || error "Git diff command failed. Check your arguments.")
fi
if [[ -z "$diff_output" ]]; then
error "No changes found to review."
fi
# Estimate token count and reduce context if needed
estimated_tokens=$(estimate_tokens "$diff_output")
if [[ $estimated_tokens -gt $MAX_TOKENS ]]; then
info "Optimizing context to fit token limits"
# Find optimal context using binary search
optimal_context=$(find_optimal_context)
info "Using context of $optimal_context lines"
# Replace unified context in git args
new_git_args=()
for arg in "${git_args[@]}"; do
if [[ "$arg" =~ ^-U[0-9]+$ ]]; then
new_git_args+=("-U$optimal_context")
elif [[ "$arg" =~ ^--unified=[0-9]+$ ]]; then
new_git_args+=("--unified=$optimal_context")
else
new_git_args+=("$arg")
fi
done
# Re-run git diff with optimal context
new_diff_output=$(git diff "${new_git_args[@]}" 2>/dev/null || error "Git diff command failed with reduced context.")
new_estimated_tokens=$(estimate_tokens "$new_diff_output")
if [[ $new_estimated_tokens -gt $MAX_TOKENS ]]; then
error "Changes are too large even with minimal context. Try reviewing fewer files."
fi
diff_output="$new_diff_output"
fi
prompt="Please review this PR as if you were a senior engineer.
## Focus Areas
- Architecture and design decisions
- Potential bugs and edge cases
- Performance considerations
- Security implications
- Code maintainability and best practices
- Test coverage
## Review Format
- Start with a brief summary of the PR purpose and changes
- List strengths of the implementation
- Identify issues and improvement opportunities (ordered by priority)
- Provide specific code examples for suggested changes where applicable
Please be specific, constructive, and actionable in your feedback. Output the review in markdown format."
# Add the additional context if provided
if [[ -n "$additional_context" ]]; then
prompt="$prompt
## Additional Context
$additional_context"
fi
if command -v bat >/dev/null 2>&1; then
echo "$diff_output" | llm -s "$prompt" | bat --paging=never --style=plain --language=markdown
else
echo "$diff_output" | llm -s "$prompt"
fi
exit_status=${PIPESTATUS[1]}
if [[ "$exit_status" -eq 130 ]]; then
# User pressed Ctrl+C, exit silently
exit 130
elif [[ "$exit_status" -ne 0 ]]; then
echo "Error: LLM command failed." >&2
exit 1
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment