602f414957
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.
148 lines
5.2 KiB
Bash
Executable File
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'
|