Skip to content

Instantly share code, notes, and snippets.

@bschne
Created July 17, 2025 07:09
Show Gist options
  • Select an option

  • Save bschne/f0c015bf3ad30f3b6e690ece9c34f8c1 to your computer and use it in GitHub Desktop.

Select an option

Save bschne/f0c015bf3ad30f3b6e690ece9c34f8c1 to your computer and use it in GitHub Desktop.
Putting this in your path gives you a git command, "git todo", that shows TODOs added between the current state of your working directory and a base branch
#!/usr/bin/env bash
set -euo pipefail
# Usage: git todo [base-branch] [--allcase] [--strict]
# Default base branch = develop.
base=${1:-develop}
shift || true
match_case=0 # 0 = case-sensitive (ignore "todos"), 1 = case-insensitive
strict=0
for arg in "$@"; do
case "$arg" in
--allcase) match_case=1 ;;
--strict) strict=1 ;;
esac
done
# Regex (POSIX BRE, portable for BSD awk)
if (( strict )); then
# Require a comment introducer before TODO (heuristic)
re='\(//\|#\|;\|%\|--\|/\*\|<!--\)[^A-Za-z0-9]*TODO\([[:space:][:punct:]]\|$\)'
else
re='TODO\([[:space:][:punct:]]\|$\)'
fi
# Resolve merge base
if ! mb=$(git merge-base "$base" HEAD 2>/dev/null); then
echo "Error: could not find merge-base with '$base'." >&2
exit 1
fi
# Portable AWK script:
# - Track current file from "+++ b/..." line (handles renames, etc.)
# - Parse new hunk start from "@@ -old +new @@"
# - Emit file:line:text for added lines beginning with '+'
awk_script='
BEGIN{file="";ln=0;}
/^\+\+\+ /{
# example: +++ b/path/to/file
file=$0;
sub(/^\+\+\+ b\//,"",file);
next;
}
/^@@ /{
# Extract +newstart
# Format: @@ -oldstart,oldlen +newstart,newlen @@ ...
# Strategy: find first "+" then read digits
start=index($0,"+");
if(start>0){
rest=substr($0,start+1);
# grab leading digits
match(rest,/^[0-9]+/);
if(RLENGTH>0){ln=substr(rest,RSTART,RLENGTH)+0;} else {ln=0;}
} else {ln=0;}
next;
}
{
c=substr($0,1,1);
if(c=="+"){
if(substr($0,1,3)=="+++") next; # skip file header
text=substr($0,2); # added line text
t=text;
if(mc){
# case-insensitive: map to upper and test upper(RE)
t=toupper(t);
if(t ~ upre){print file ":" ln ":" text;}
} else {
if(text ~ re){print file ":" ln ":" text;}
}
ln++;
} else if(c=="-"){
# removed line; do not increment ln
next;
} else {
# context line
ln++;
}
}
'
# For case-insensitive mode we upper() both text and pattern.
# Build uppercase version of the pattern (crude but works for ASCII TODO).
upre=$(printf '%s\n' "$re" | tr '[:lower:]' '[:upper:]')
# Force raw, parseable diff (ignore user diff.external / tool / textconv)
DIFF_BASE_OPTS=(-c diff.external= -c diff.tool= diff --no-ext-diff --no-textconv --no-color -U0)
run_scan () {
local label=$1; shift
echo "=== $label ==="
if ! git "${DIFF_BASE_OPTS[@]}" "$@" | awk -vre="$re" -vupre="$upre" -vmc="$match_case" "$awk_script"; then
echo "(error running diff)" >&2
fi
echo
}
run_scan "Working tree (staged + unstaged vs HEAD)" HEAD
run_scan "Commits since $base (merge-base $mb)" "$mb"...HEAD
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment