Last active
August 4, 2025 00:01
-
-
Save emaballarin/d238ef3724110d0cbd9577dde64eb176 to your computer and use it in GitHub Desktop.
Tool for AI-powered code review, wrapping around git diff
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
#!/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