166 lines
4.6 KiB
Bash
166 lines
4.6 KiB
Bash
#!/usr/bin/env bash
|
||
# Changelog sync guard:按活动扫描配置提示本会话内尚未提交的项目改动。
|
||
|
||
emit_empty() {
|
||
printf '{}\n'
|
||
}
|
||
|
||
cat >/dev/null
|
||
|
||
if [ -n "${CURSOR_SKIP_CHANGELOG:-}" ]; then
|
||
emit_empty
|
||
exit 0
|
||
fi
|
||
|
||
script_dir=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)
|
||
cursor_dir=$(dirname -- "$script_dir")
|
||
repo_root=$(dirname -- "$cursor_dir")
|
||
changelog="$cursor_dir/changelog/changelog-headlines.md"
|
||
ack_file="$cursor_dir/changelog/.changelog-ack"
|
||
session_marker="$cursor_dir/changelog/.session-start"
|
||
scan_config="$script_dir/changelog-scan.json"
|
||
|
||
if [ ! -f "$changelog" ] || [ ! -f "$scan_config" ]; then
|
||
emit_empty
|
||
exit 0
|
||
fi
|
||
|
||
if [ ! -f "$session_marker" ]; then
|
||
emit_empty
|
||
exit 0
|
||
fi
|
||
|
||
get_mtime() {
|
||
stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null
|
||
}
|
||
|
||
if [ -f "$ack_file" ]; then
|
||
changelog_mtime=$(get_mtime "$changelog")
|
||
ack_mtime=$(get_mtime "$ack_file")
|
||
if [ -n "$changelog_mtime" ] && [ -n "$ack_mtime" ] &&
|
||
[ "$ack_mtime" -ge "$changelog_mtime" ] 2>/dev/null; then
|
||
emit_empty
|
||
exit 0
|
||
fi
|
||
fi
|
||
|
||
python_cmd=''
|
||
for candidate in python3 python; do
|
||
if command -v "$candidate" >/dev/null 2>&1 &&
|
||
"$candidate" -c 'import json' >/dev/null 2>&1; then
|
||
python_cmd=$candidate
|
||
break
|
||
fi
|
||
done
|
||
|
||
if [ -z "$python_cmd" ]; then
|
||
emit_empty
|
||
exit 0
|
||
fi
|
||
|
||
result=$(
|
||
"$python_cmd" - "$repo_root" "$changelog" "$session_marker" "$scan_config" <<'PY'
|
||
import json
|
||
import os
|
||
from pathlib import Path
|
||
import subprocess
|
||
import sys
|
||
|
||
repo_root = Path(sys.argv[1]).resolve()
|
||
changelog = Path(sys.argv[2])
|
||
session_marker = Path(sys.argv[3])
|
||
scan_config = Path(sys.argv[4])
|
||
|
||
def emit(value):
|
||
print(json.dumps(value, ensure_ascii=False, separators=(",", ":")))
|
||
|
||
try:
|
||
config = json.loads(scan_config.read_text(encoding="utf-8"))
|
||
if config.get("schema_version") != 1:
|
||
raise ValueError("unsupported schema")
|
||
names = {
|
||
value.casefold()
|
||
for value in config.get("included_names", [])
|
||
if isinstance(value, str) and value
|
||
}
|
||
extensions = {
|
||
value.casefold()
|
||
for value in config.get("included_extensions", [])
|
||
if isinstance(value, str) and value
|
||
}
|
||
excluded = {
|
||
value.casefold()
|
||
for value in config.get("excluded_directories", [])
|
||
if isinstance(value, str) and value
|
||
}
|
||
if not names and not extensions:
|
||
emit({})
|
||
raise SystemExit(0)
|
||
|
||
status = subprocess.run(
|
||
["git", "-C", str(repo_root), "status", "--porcelain=v1",
|
||
"--untracked-files=all", "--"],
|
||
check=False,
|
||
capture_output=True,
|
||
text=True,
|
||
encoding="utf-8",
|
||
)
|
||
if status.returncode != 0 or not status.stdout.strip():
|
||
emit({})
|
||
raise SystemExit(0)
|
||
|
||
comparison_mtime = max(
|
||
changelog.stat().st_mtime,
|
||
session_marker.stat().st_mtime,
|
||
)
|
||
for current_root, directory_names, file_names in os.walk(repo_root):
|
||
directory_names[:] = [
|
||
name for name in directory_names if name.casefold() not in excluded
|
||
]
|
||
current = Path(current_root)
|
||
for file_name in file_names:
|
||
candidate = current / file_name
|
||
if (
|
||
file_name.casefold() not in names
|
||
and candidate.suffix.casefold() not in extensions
|
||
):
|
||
continue
|
||
try:
|
||
if candidate.stat().st_mtime <= comparison_mtime:
|
||
continue
|
||
except OSError:
|
||
continue
|
||
relative = candidate.relative_to(repo_root).as_posix()
|
||
path_status = subprocess.run(
|
||
["git", "-C", str(repo_root), "status", "--porcelain=v1",
|
||
"--untracked-files=all", "--", relative],
|
||
check=False,
|
||
capture_output=True,
|
||
text=True,
|
||
encoding="utf-8",
|
||
)
|
||
if path_status.returncode == 0 and path_status.stdout.strip():
|
||
message = (
|
||
"[Hook] Project source/config has an uncommitted change newer "
|
||
f"than this session and the changelog (e.g. {relative}). "
|
||
"Run dev-changelog Skill operation A now: add a fragment under "
|
||
".cursor/changelog/entries/ and rebuild the changelog views."
|
||
)
|
||
emit({"followup_message": message})
|
||
raise SystemExit(0)
|
||
emit({})
|
||
except SystemExit:
|
||
raise
|
||
except Exception:
|
||
emit({})
|
||
PY
|
||
) || result='{}'
|
||
|
||
if [ -n "$result" ]; then
|
||
printf '%s\n' "$result"
|
||
else
|
||
emit_empty
|
||
fi
|
||
|
||
exit 0
|