|
| 1 | +#!/usr/bin/env bash |
| 2 | +# Filter `git blame` by cutoff time; --older flips the comparison. |
| 3 | +# Requires: bash 4+, git, GNU date (coreutils). |
| 4 | +set -euo pipefail |
| 5 | + |
| 6 | +PROG=${0##*/} |
| 7 | +DATE_BIN=${DATE_BIN:-date} |
| 8 | + |
| 9 | +BH_TMPFILE="" |
| 10 | +trap '[[ -n $BH_TMPFILE ]] && rm -f -- "$BH_TMPFILE"' EXIT |
| 11 | + |
| 12 | +die() { printf 'error: %s\n' "$*" >&2; exit 1; } |
| 13 | + |
| 14 | +usage() { |
| 15 | + cat >&2 <<EOF |
| 16 | +Usage: $PROG --since <date|datetime> --file <path> [--older] [--csv] |
| 17 | + $PROG -h | --help |
| 18 | +
|
| 19 | +Options: |
| 20 | + --since <when> Required. YYYY-MM-DD (UTC midnight) or any GNU date string. |
| 21 | + --file, -f <p> Required. Path passed to \`git blame\`. |
| 22 | + --older Show lines last changed BEFORE --since (default: on/after). |
| 23 | + --csv Emit CSV (header + RFC 4180 quoting) instead of text. |
| 24 | + -h, --help Show this help and exit. |
| 25 | +
|
| 26 | +Requires: bash 4+, git, GNU date. |
| 27 | +EOF |
| 28 | +} |
| 29 | + |
| 30 | +# Bare YYYY-MM-DD is normalized to 00:00:00 UTC that day (same as README). |
| 31 | +since_epoch() { |
| 32 | + local s=$1 out |
| 33 | + [[ $s =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] && s="$s 00:00:00" |
| 34 | + out=$("$DATE_BIN" -u -d "$s" +%s 2>/dev/null) || true |
| 35 | + [[ $out =~ ^[0-9]+$ ]] || die "invalid --since: not a valid GNU date string: $1" |
| 36 | + printf '%s\n' "$out" |
| 37 | +} |
| 38 | + |
| 39 | +# CSV doubles embedded quotes (RFC 4180). |
| 40 | +emit_row() { |
| 41 | + local mode=$1 ts=$2 author=$3 line=$4 line_no=$5 sha=$6 day qa ql |
| 42 | + day=$("$DATE_BIN" -u -d "@$ts" +%F) |
| 43 | + if ((mode)); then |
| 44 | + qa=${author//\"/\"\"} |
| 45 | + ql=${line:1}; ql=${ql//\"/\"\"} |
| 46 | + printf '%s,%s,"%s",%d,"%s"\n' "${sha:0:8}" "$day" "$qa" "$line_no" "$ql" |
| 47 | + else |
| 48 | + printf '%s (%s, %s) %6d %s\n' "${sha:0:8}" "$day" "$author" "$line_no" "${line:1}" |
| 49 | + fi |
| 50 | +} |
| 51 | + |
| 52 | +main() { |
| 53 | + local since="" path="" older=0 csv=0 cutoff sha line_no author="" author_time="" committer_time="" ts n=0 line |
| 54 | + while (($#)); do |
| 55 | + case $1 in |
| 56 | + --since) |
| 57 | + (($# >= 2)) || { usage; die "--since requires a value"; } |
| 58 | + since=$2 |
| 59 | + shift 2 |
| 60 | + ;; |
| 61 | + --file | -f) |
| 62 | + (($# >= 2)) || { usage; die "--file requires a path"; } |
| 63 | + path=$2 |
| 64 | + shift 2 |
| 65 | + ;; |
| 66 | + --older) older=1; shift ;; |
| 67 | + --csv) csv=1; shift ;; |
| 68 | + -h | --help) usage; exit 0 ;; |
| 69 | + *) usage; die "unexpected argument: $1 (use --file/-f for the path)" ;; |
| 70 | + esac |
| 71 | + done |
| 72 | + [[ -n $since ]] || { usage; die "--since is required"; } |
| 73 | + [[ -n $path ]] || { usage; die "--file is required"; } |
| 74 | + |
| 75 | + command -v git >/dev/null 2>&1 || die "git not found in PATH" |
| 76 | + if ! "$DATE_BIN" --version >/dev/null 2>&1; then |
| 77 | + if command -v gdate >/dev/null 2>&1 && gdate --version >/dev/null 2>&1; then |
| 78 | + DATE_BIN=gdate |
| 79 | + else |
| 80 | + die "GNU date required; install coreutils or set DATE_BIN=/path/to/gdate" |
| 81 | + fi |
| 82 | + fi |
| 83 | + |
| 84 | + cutoff=$(since_epoch "$since") |
| 85 | + |
| 86 | + BH_TMPFILE=$(mktemp "${TMPDIR:-/tmp}/${PROG}.XXXXXX") || die "mktemp failed" |
| 87 | + git blame --line-porcelain -- "$path" >"$BH_TMPFILE" || die "git blame failed" |
| 88 | + ((csv)) && echo 'sha,date,author,line_no,line_text' |
| 89 | + |
| 90 | + # Porcelain records end with the TAB-prefixed source line. |
| 91 | + while IFS= read -r line || [[ -n $line ]]; do |
| 92 | + case $line in |
| 93 | + author\ *) author=${line#author } ;; |
| 94 | + author-time\ *) author_time=${line#author-time } ;; |
| 95 | + committer-time\ *) committer_time=${line#committer-time } ;; |
| 96 | + $'\t'*) |
| 97 | + ts=${author_time:-${committer_time:-0}} |
| 98 | + if (( (ts >= cutoff) ^ older )); then |
| 99 | + emit_row "$csv" "$ts" "${author:-?}" "$line" "$line_no" "$sha" |
| 100 | + ((++n)) |
| 101 | + fi |
| 102 | + ;; |
| 103 | + *) |
| 104 | + # Header: SHA, source line no., final line no. |
| 105 | + [[ $line =~ ^([0-9a-fA-F]{40}|[0-9a-fA-F]{64})[[:space:]]+[0-9]+[[:space:]]+([0-9]+) ]] && { |
| 106 | + sha=${BASH_REMATCH[1],,} |
| 107 | + line_no=${BASH_REMATCH[2]} |
| 108 | + author="" author_time="" committer_time="" |
| 109 | + } |
| 110 | + ;; |
| 111 | + esac |
| 112 | + done <"$BH_TMPFILE" |
| 113 | + |
| 114 | + ((n)) || echo "No matching lines." >&2 |
| 115 | +} |
| 116 | + |
| 117 | +main "$@" |
0 commit comments