Skip to content

Commit 0daabeb

Browse files
committed
Update git-commit-push-scriptsh to safely escape JSON strings using
1 parent a545988 commit 0daabeb

File tree

1 file changed

+43
-78
lines changed

1 file changed

+43
-78
lines changed

β€Žgit-commit-push-script.shβ€Ž

Lines changed: 43 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,18 @@ BOLD='\033[1m'
2929
DIM='\033[2m'
3030
NC='\033[0m' # No Color
3131

32+
# Escape a string for safe embedding in a JSON value.
33+
# Uses only bash parameter expansion β€” zero subprocess forks.
34+
_json_str() {
35+
local s="${1//\\/\\\\}" # \ β†’ \\
36+
local _q='"'
37+
s="${s//$_q/\\$_q}" # " β†’ \"
38+
s="${s//$'\n'/\\n}"
39+
s="${s//$'\t'/\\t}"
40+
s="${s//$'\r'/\\r}"
41+
printf '%s' "$s"
42+
}
43+
3244
# Snake spinner β€” loops forever until killed by the caller.
3345
# Usage: snake_spinner [label]
3446
# Run in background (&), capture PID, kill after the task completes.
@@ -214,101 +226,54 @@ if [ -n "$SQUISH_BIN" ]; then
214226
stat_summary=$(git diff --cached --stat | tail -1)
215227
changed_names=$(git diff --cached --name-only | head -10 | tr '\n' ' ')
216228

217-
# Write diff to a temp file so Python reads it safely
218-
echo "$diff" > /tmp/squish_diff.txt
219-
220-
# Use python3 to build the JSON payload β€” all values go through
221-
# json.dumps() so control characters are properly escaped.
222-
PAYLOAD=$(SQUISH_CHANGED="$changed_names" SQUISH_STAT="$stat_summary" MAX_DIFF_CHARS="$MAX_DIFF_CHARS" \
223-
python3 - <<'PYEOF'
224-
import json, os, re
225-
226-
def strip_diff(raw: str, max_chars: int) -> str:
227-
"""Keep only added/removed lines; skip headers and unchanged context."""
228-
lines = []
229-
for line in raw.splitlines():
230-
# +++ / --- are file headers β€” skip
231-
if line.startswith("---") or line.startswith("+++"):
232-
continue
233-
# @@ hunk headers β€” include as section markers but shorten
234-
if line.startswith("@@"):
235-
lines.append(line.split("@@")[-1].strip() or "~~")
236-
continue
237-
# diff --git / index headers β€” skip
238-
if line.startswith("diff ") or line.startswith("index ") or line.startswith("new file") or line.startswith("deleted file"):
239-
continue
240-
# Keep + / - changed lines, drop unchanged context lines
241-
if line.startswith("+") or line.startswith("-"):
242-
lines.append(line)
243-
return "\n".join(lines)[:max_chars]
244-
245-
diff_raw = open("/tmp/squish_diff.txt").read()
246-
diff = strip_diff(diff_raw, int(os.environ.get("MAX_DIFF_CHARS", "1200")))
247-
248-
system = (
249-
"You are a git commit message writer. "
250-
"Read the diff and write ONE concise commit message describing what actually changed. "
251-
"Reply with ONLY the commit message β€” no labels, no filenames, no markdown, no period. "
252-
"Must be a complete thought under 72 characters. Imperative mood (e.g. 'Add', 'Fix', 'Update', 'Remove')."
253-
)
254-
user = (
255-
f"Files: {os.environ['SQUISH_CHANGED']}\n"
256-
f"Stat: {os.environ['SQUISH_STAT']}\n\n"
257-
f"Changed lines:\n{diff}\n"
258-
"--- END DIFF ---\n\n"
259-
"Commit message (imperative, < 72 chars):"
260-
)
261-
print(json.dumps({
262-
"model": "squish",
263-
"messages": [
264-
{"role": "system", "content": system},
265-
{"role": "user", "content": user},
266-
],
267-
"max_tokens": 50,
268-
"temperature": 0.2,
269-
"stream": False,
270-
"stop": ["\n", "\r"],
271-
}))
272-
PYEOF
273-
)
274-
rm -f /tmp/squish_diff.txt
275-
276-
# Run squish with timeout and spinner
229+
# Strip diff to +/- lines only (awk, no Python)
230+
stripped_diff=$(git diff --cached | awk '
231+
/^---/ || /^\+\+\+/ { next }
232+
/^diff / || /^index / || /^new file/ || /^deleted file/ { next }
233+
/^@@/ { sub(/^@@[^@]*@@ */, ""); print (length($0)>0 ? $0 : "~~"); next }
234+
/^\+/ || /^-/ { print }
235+
' | head -c "$MAX_DIFF_CHARS")
236+
237+
# Build JSON payload in pure bash β€” _json_str escapes all special chars
238+
_sys="You are a git commit message writer. Read the diff and write ONE concise commit message describing what actually changed. Reply with ONLY the commit message β€” no labels, no filenames, no markdown, no period. Must be a complete thought under 72 characters. Imperative mood (e.g. 'Add', 'Fix', 'Update', 'Remove')."
239+
_usr="Files: ${changed_names}\nStat: ${stat_summary}\n\nChanged lines:\n${stripped_diff}\n--- END DIFF ---\n\nCommit message (imperative, < 72 chars):"
240+
PAYLOAD='{"model":"squish","messages":[{"role":"system","content":"'"$(_json_str "$_sys")"'"},{"role":"user","content":"'"$(_json_str "$_usr")"'"}],"max_tokens":50,"temperature":0.2,"stream":false,"stop":["\n","\r"]}'
241+
242+
# Run squish β€” curl in background, spinner inline in foreground (no subprocess)
277243
print_step "Asking AI for commit message (Squish local LLM)..."
278244
_port="${SQUISH_PORT:-11435}"
279-
_llm_start=$SECONDS
280245
curl -s --max-time $TIMEOUT_SECONDS \
281246
-X POST "http://127.0.0.1:${_port}/v1/chat/completions" \
282247
-H "Content-Type: application/json" \
283248
-H "Authorization: Bearer ${SQUISH_API_KEY:-squish}" \
284249
-d "$PAYLOAD" 2>/tmp/squish_stderr.txt \
285250
> /tmp/squish_response.txt &
286251
LLM_PID=$!
287-
# Spinner runs in background; wait reaps curl immediately in foreground
288-
snake_spinner "Generating commit message" &
289-
_SPINNER_PID=$!
290-
wait $LLM_PID
252+
# Inline spinner β€” runs in the main shell, no subprocess, no background PID
253+
_sp_frames=('⣾' '⣽' '⣻' 'Ⓙ' '⑿' '⣟' '⣯' '⣷')
254+
_sp_cols=("$CYAN" "$BLUE" "$PURPLE" "$CYAN" "$BLUE" "$PURPLE" "$CYAN" "$BLUE")
255+
_si=0; _step=0
256+
while kill -0 "$LLM_PID" 2>/dev/null; do
257+
_secs=$(( _step / 10 )); _tenths=$(( _step % 10 ))
258+
printf "\r${_sp_cols[$_si]}${_sp_frames[$_si]}${NC} ${WHITE}Generating commit message${NC}${GRAY}...${NC} ${DIM}(${_secs}.${_tenths}s)${NC} "
259+
read -t 0.1 </dev/null 2>/dev/null || true
260+
_si=$(( (_si + 1) % 8 ))
261+
_step=$(( _step + 1 ))
262+
done
263+
wait $LLM_PID # reap immediately β€” zombie cleared within one loop tick
291264
exit_code=$?
292-
kill $_SPINNER_PID 2>/dev/null
293-
wait $_SPINNER_PID 2>/dev/null
294265
printf "\r${GREEN}βœ“${NC} ${WHITE}Done!${NC} \n"
295-
_llm_elapsed=$(( SECONDS - _llm_start ))
296-
print_info "model response time: ${CYAN}${_llm_elapsed}s${NC}"
266+
# step is already in tenths of a second β€” free decimal timing, no extra call
267+
_llm_secs=$(( _step / 10 )); _llm_tenths=$(( _step % 10 ))
268+
print_info "model response time: ${CYAN}${_llm_secs}.${_llm_tenths}s${NC}"
297269

298270
# ── Debug: result diagnostics ─────────────────────────────────────────
299271
raw_response=$(cat /tmp/squish_response.txt 2>/dev/null)
300272
squish_stderr=$(cat /tmp/squish_stderr.txt 2>/dev/null)
301273
rm -f /tmp/squish_response.txt /tmp/squish_stderr.txt
302274

303-
# Extract the message content from the JSON response
304-
commit_message=$(echo "$raw_response" | python3 -c "
305-
import sys, json
306-
try:
307-
data = json.load(sys.stdin)
308-
print(data['choices'][0]['message']['content'].strip())
309-
except Exception:
310-
pass
311-
" 2>/dev/null)
275+
# Extract content field from JSON response with sed β€” no Python
276+
commit_message=$(printf '%s' "$raw_response" | sed 's/.*"content":"\([^"]*\)".*/\1/' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
312277

313278
print_info "squish exit code: ${CYAN}$exit_code${NC}"
314279
if [ -n "$commit_message" ]; then

0 commit comments

Comments
Β (0)