diff --git a/hooks/commit_msg.sh b/hooks/commit_msg.sh new file mode 100755 index 0000000..4aa2345 --- /dev/null +++ b/hooks/commit_msg.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash +# commit_msg.sh — génère un message de commit depuis les changements en cours +# Fonctionne avec git (fichiers stagés) et jj (working copy). +# +# Usage : bash .claude/hooks/commit_msg.sh + +set -euo pipefail + +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; } + +# --- Détection du VCS --- + +_detect_vcs() { + if jj root &>/dev/null; then + echo "jj" + elif git rev-parse --git-dir &>/dev/null; then + echo "git" + else + echo "none" + fi +} + +# --- Fichiers modifiés --- + +_changed_files() { + case "$VCS" in + jj) jj diff --name-only ;; + git) git diff --staged --name-only ;; + esac +} + +# --- Diff lisible par fichier --- + +_readable_diff() { + local file="$1" + local raw_diff + + case "$VCS" in + jj) raw_diff=$(jj diff -- "$file") ;; + git) raw_diff=$(git diff --staged -- "$file") ;; + esac + + [[ -z "$raw_diff" ]] && return 0 + + local max_len + max_len=$(grep '^[+-]' <<< "$raw_diff" 2>/dev/null \ + | awk '{ if (length > m) m = length } END { print m+0 }') + + if (( max_len <= 500 )); then + printf '%s' "$raw_diff" + return + fi + + # Pretty-print pour les formats pathologiques (JSON, JS minifié…) + local ext="${file##*.}" + local pretty_old pretty_new + + case "$ext" in + json) + case "$VCS" in + jj) + pretty_old=$(jj file show "${file}@-" 2>/dev/null | python3 -m json.tool 2>/dev/null || true) + pretty_new=$(jj file show "$file" 2>/dev/null | python3 -m json.tool 2>/dev/null || true) + ;; + git) + pretty_old=$(git show "HEAD:${file}" 2>/dev/null | python3 -m json.tool 2>/dev/null || true) + pretty_new=$(git show ":${file}" 2>/dev/null | python3 -m json.tool 2>/dev/null || true) + ;; + esac + ;; + 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(""); + const out = src + .replace(/([{(])/g, "$1\n ") + .replace(/([;}])/g, "\n$1\n") + .replace(/,\s*/g, ",\n "); + process.stdout.write(out); + });' + case "$VCS" in + jj) + pretty_old=$(jj file show "${file}@-" 2>/dev/null | node -e "$node_fmt" 2>/dev/null || true) + pretty_new=$(jj file show "$file" 2>/dev/null | node -e "$node_fmt" 2>/dev/null || true) + ;; + git) + pretty_old=$(git show "HEAD:${file}" 2>/dev/null | node -e "$node_fmt" 2>/dev/null || true) + pretty_new=$(git show ":${file}" 2>/dev/null | node -e "$node_fmt" 2>/dev/null || true) + ;; + esac + ;; + *) + case "$VCS" in + jj) + pretty_old=$(jj file show "${file}@-" 2>/dev/null | fold -s -w 120 || true) + pretty_new=$(jj file show "$file" 2>/dev/null | fold -s -w 120 || true) + ;; + git) + pretty_old=$(git show "HEAD:${file}" 2>/dev/null | fold -s -w 120 || true) + pretty_new=$(git show ":${file}" 2>/dev/null | fold -s -w 120 || true) + ;; + esac + ;; + 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 +} + +# --- Main --- + +VCS=$(_detect_vcs) + +if [[ "$VCS" == "none" ]]; then + echo "ERREUR : ni git ni jj détecté dans ce répertoire." >&2 + exit 1 +fi + +log "VCS détecté : $VCS" + +changed_files=$(_changed_files) + +if [[ -z "$changed_files" ]]; then + echo "Aucun fichier modifié." >&2 + exit 1 +fi + +file_count=$(wc -l <<< "$changed_files" | tr -d ' ') +log "Fichiers modifiés : $file_count" + +summaries="" +n=0 + +while IFS= read -r file; do + diff=$(_readable_diff "$file") + [[ -z "$diff" ]] && continue + + n=$((n + 1)) + log "[$n/$file_count] Résumé de $file …" + + summary=$(printf '%s' "$diff" \ + | aichat "In 2-3 lines, summarise what this diff changes in the file '$file'. Be concise and technical.") + + while IFS= read -r line; do info "$line"; done <<< "$summary" + + summaries+="### $file +$summary + +" +done <<< "$changed_files" + +if [[ -z "$summaries" ]]; then + echo "Aucun diff non-vide trouvé." >&2 + exit 1 +fi + +log "Génération du message de commit ($n fichier(s)) …" + +result=$(printf '%s' "$summaries" \ + | aichat "From these per-file summaries of a diff, write a single conventional git 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.") + +ok "Terminé" +printf '\n' >&2 + +printf '%s\n' "$result" diff --git a/skills/commit-message.md b/skills/commit-message.md new file mode 100644 index 0000000..cbc9e68 --- /dev/null +++ b/skills/commit-message.md @@ -0,0 +1,23 @@ +--- +name: commit-message +description: Génère un message de commit depuis les changements en cours (git ou jj) via aichat +--- + +# Génération de message de commit + +Quand l'utilisateur demande de générer un message de commit ou de committer : + +1. Lance le script : + ```sh + bash .claude/hooks/commit_msg.sh + ``` + Le script détecte automatiquement git ou jj, résume chaque fichier modifié via aichat, + puis génère un message de commit conventionnel. + +2. Présente le message généré à l'utilisateur pour validation. + +3. Si l'utilisateur valide, effectue le commit : + - **git** : `git commit -m ""` + - **jj** : `jj describe -m ""` + +Le script écrit sa progression sur stderr et le message final sur stdout.