Files
Eric Coissac 602f414957 fix: strip AI reasoning blocks from commit messages
Adds a `_strip_think` function using `awk` to buffer stdin and track the last `</think>` tag, emitting only the subsequent content. This utility is now piped after `aichat` calls to remove AI reasoning blocks before commit message generation. Also applies minor whitespace and indentation adjustments throughout the script.
2026-05-03 17:42:17 +02:00

148 lines
5.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# jj_commit_msg.sh — generate a commit message from the current jj change using aichat
#
# Usage: jj_commit_msg.sh [REV]
# Summarises each changed file's diff individually, then combines all
# summaries into a single commit message via aichat.
# REV defaults to `@` (current working copy). Accepts any jj revision:
# `@-`, `lk`, a commit ID, a branch name, etc.
#
# Typical use:
# jj describe -m "$(jj_commit_msg.sh)"
# jj describe -m "$(jj_commit_msg.sh @-)"
# jj describe -m "$(jj_commit_msg.sh lk)"
set -euo pipefail
# Optional revision to diff (default: @ = current working copy)
REV="${1:-@}"
# Log to stderr so progress doesn't pollute the commit message on stdout
log() { printf '\033[1;34m==>\033[0m %s\n' "$*" >&2; }
info() { printf ' \033[0;37m%s\033[0m\n' "$*" >&2; }
ok() { printf ' \033[0;32m✓\033[0m %s\n' "$*" >&2; }
# _strip_think — remove reasoning tags from stdin
# Buffer all input, locate the LAST </think> line, emit only what follows.
# This handles think blocks that themselves contain </think> fragments.
_strip_think() {
awk '{
lines[NR] = $0
print $0 > "/dev/stderr"
if (/^<\/think>/) last_end = NR
}
END {
start = (last_end ? last_end + 1 : 1)
for (i = start; i <= NR; i++) print lines[i]
}'
}
# _readable_diff <file>
# Returns a human-readable diff for <file>.
# For pathological single-line formats (JSON, minified JS/CSS…), pretty-prints
# both the parent and working versions before diffing so the LLM sees
# structured changes rather than one enormous ±line.
_readable_diff() {
local file="$1"
local raw_diff
raw_diff=$(jj diff -r "$REV" -- "$file")
[[ -z "$raw_diff" ]] && return 0
# Detect pathological diff: any +/- content line longer than 500 chars
local max_len
max_len=$(grep '^[+-]' <<< "$raw_diff" | awk '{ if (length > m) m = length } END { print m+0 }')
if (( max_len <= 500 )); then
printf '%s' "$raw_diff"
return
fi
# Pretty-print strategy per extension
local ext="${file##*.}"
local pretty_old pretty_new
case "$ext" in
json)
pretty_old=$(jj file show -r "$REV@-" -- "$file" 2>/dev/null | python3 -m json.tool 2>/dev/null || true)
pretty_new=$(jj file show -r "$REV" -- "$file" 2>/dev/null | python3 -m json.tool 2>/dev/null || true)
;;
js|mjs|cjs|css|ts)
local node_fmt='
const chunks = [];
process.stdin.on("data", d => chunks.push(d));
process.stdin.on("end", () => {
const src = chunks.join("");
// Insert newline before { } ( ) ; and after ,
const out = src
.replace(/([{(])/g, "$1\n ")
.replace(/([;}])/g, "\n$1\n")
.replace(/,\s*/g, ",\n ");
process.stdout.write(out);
});'
pretty_old=$(jj file show -r "$REV@-" -- "$file" 2>/dev/null | node -e "$node_fmt" 2>/dev/null || true)
pretty_new=$(jj file show -r "$REV" -- "$file" 2>/dev/null | node -e "$node_fmt" 2>/dev/null || true)
;;
*)
# Generic fallback: fold long lines at 120 chars
pretty_old=$(jj file show -r "$REV@-" -- "$file" 2>/dev/null | fold -s -w 120 || true)
pretty_new=$(jj file show -r "$REV" -- "$file" 2>/dev/null | fold -s -w 120 || true)
;;
esac
if [[ -n "$pretty_old" && -n "$pretty_new" ]]; then
diff <(printf '%s\n' "$pretty_old") <(printf '%s\n' "$pretty_new") \
--label "a/${file}" --label "b/${file}" -u || true
else
printf '%s' "$raw_diff"
fi
}
# Collect changed files in the current working copy change
changed_files=$(jj diff -r "$REV" --name-only)
if [[ -z "$changed_files" ]]; then
echo "No changed files." >&2
exit 1
fi
file_count=$(wc -l <<< "$changed_files" | tr -d ' ')
log "Found $file_count changed file(s)"
summaries=""
n=0
while IFS= read -r file; do
diff=$(_readable_diff "$file")
if [[ -z "$diff" ]]; then
continue
fi
n=$((n + 1))
log "[$n/$file_count] Summarising $file"
summary=$(printf '%s' "$diff" | aichat "In 2-3 lines, summarise what this diff changes in the file '$file'. Be concise and technical." | _strip_think)
# Print the summary indented to stderr
while IFS= read -r line; do
info "$line"
done <<< "$summary"
summaries+="### $file
$summary
"
done <<< "$changed_files"
if [[ -z "$summaries" ]]; then
echo "No non-empty diffs found." >&2
exit 1
fi
log "Generating commit message from $n summary/summaries …"
result=$(printf '%s' "$summaries" | aichat "From these per-file summaries of a jj diff, write a single conventional commit message in English. First line: short imperative summary (max 72 chars). Then a blank line. Then a short paragraph with more detail if needed. Output only the commit message, nothing else." | _strip_think)
ok "Done"
printf '\n' >&2
# Commit message goes to stdout (strip leading blank lines so jj sees content)
printf '%s\n' "$result" | sed '/./,$!d'