已合并
fix pre-commit file change hook #92
fix pre-commit file change hook #92
已合并
chenyx_2012创建于 6月24日
共 1 个文件变更+214-39
@@ -48,15 +48,49 @@ if [ -z "$_PYTHON" ]; then
48fi48fi
49 49 
50# ---------------------------------------------------------------------------50# ---------------------------------------------------------------------------
51-# 1. Ensure oat-py is installed51+# 1. Ensure oat-py>=1.0.2 is installed (serialized via flock to prevent parallel pip conflicts)
52# ---------------------------------------------------------------------------52# ---------------------------------------------------------------------------
53-_OAT_OK=$("$_PYTHON" -c "import importlib.util; print('ok' if importlib.util.find_spec('oat') else 'missing')" 2>/dev/null || echo "missing")53+_OAT_MIN="1.0.2"
54+_check_oat_version() {
55+ "$_PYTHON" -c "
56+import importlib.util, sys
57+if not importlib.util.find_spec('oat'):
58+ print('missing'); sys.exit()
59+try:
60+ from importlib.metadata import version
61+ v = version('oat-py')
62+ parts_have = [int(x) for x in v.split('.')[:3]]
63+ parts_need = [int(x) for x in '${_OAT_MIN}'.split('.')[:3]]
64+ print('ok' if parts_have >= parts_need else 'old')
65+except Exception:
66+ print('ok') # cannot determine version, assume ok
67+" 2>/dev/null || echo "missing"
68+}
69+ 
70+_OAT_OK=$(_check_oat_version)
54if [ "$_OAT_OK" != "ok" ]; then71if [ "$_OAT_OK" != "ok" ]; then
55- echo "[OAT] oat-py not found. Installing oat-py>=1.0.1 ..."72+ if [ "$_OAT_OK" = "old" ]; then
56- "$_PYTHON" -m pip install --quiet "oat-py>=1.0.1"73+ echo "[OAT] oat-py is outdated. Upgrading to oat-py>=${_OAT_MIN} ..."
57- _OAT_OK=$("$_PYTHON" -c "import importlib.util; print('ok' if importlib.util.find_spec('oat') else 'missing')" 2>/dev/null || echo "missing")74+ else
75+ echo "[OAT] oat-py not found. Installing oat-py>=${_OAT_MIN} ..."
76+ fi
77+ _LOCK_FILE="/tmp/oat_pip_install.lock"
78+ # flock ensures only one concurrent invocation runs pip install at a time;
79+ # re-check inside the lock so processes that waited skip redundant installs
80+ if command -v flock >/dev/null 2>&1; then
81+ (
82+ flock -w 120 9
83+ _RECHECK=$(_check_oat_version)
84+ if [ "$_RECHECK" != "ok" ]; then
85+ "$_PYTHON" -m pip install --quiet "oat-py>=${_OAT_MIN}"
86+ fi
87+ ) 9>"$_LOCK_FILE"
88+ else
89+ "$_PYTHON" -m pip install --quiet "oat-py>=${_OAT_MIN}"
90+ fi
91+ _OAT_OK=$(_check_oat_version)
58 if [ "$_OAT_OK" != "ok" ]; then92 if [ "$_OAT_OK" != "ok" ]; then
59- echo "[OAT] [WARNING] Failed to install oat-py. Please run: pip install oat-py>=1.0.1"93+ echo "[OAT] [WARNING] Failed to install oat-py. Please run: pip install oat-py>=${_OAT_MIN}"
60 echo "[OAT] Skipping OAT check, continuing commit..."94 echo "[OAT] Skipping OAT check, continuing commit..."
61 exit 095 exit 0
62 fi96 fi
@@ -68,10 +102,99 @@ fi
68# ---------------------------------------------------------------------------102# ---------------------------------------------------------------------------
69REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)103REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
70REPO_NAME=$(basename "$REPO_ROOT")104REPO_NAME=$(basename "$REPO_ROOT")
71-OAT_REPORT_DIR="$REPO_ROOT/oat_reports"105+OAT_REPORT_DIR="${TMPDIR:-/tmp}/oat_reports_$$"
106+OAT_RESULT_DIR="$REPO_ROOT/oat_reports"
72 107 
73-echo "[OAT] Running OAT scan (Python Edition) — INCREMENTAL MODE"108+# ---------------------------------------------------------------------------
109+# 2a. PR-range deduplication (pure git, no CI env vars required)
110+#
111+# Strategy:
112+# 1. Find the merge-base between HEAD and the upstream branch (origin/master
113+# or similar). If found, this is a feature-branch context ??collect ALL
114+# files changed since the branch diverged (full PR diff).
115+# 2. Key a done-marker on the HEAD SHA. The first invocation runs the scan;
116+# every subsequent invocation for the same HEAD exits 0 immediately.
117+# This eliminates redundant scans when CI calls the hook once per commit.
118+# 3. If no merge-base is found (e.g. committing directly on master) fall back
119+# to scanning only the currently staged files, with no done-marker.
120+# ---------------------------------------------------------------------------
121+_PR_MERGE_BASE=""
122+_DONE_MARKER=""
123+_HEAD_SHA=$(git rev-parse HEAD 2>/dev/null | cut -c1-12 || true)
124+ 
125+if [ -n "$_HEAD_SHA" ]; then
126+ # Try to find merge-base with a known upstream branch
127+ if [ -n "${PRE_COMMIT_FROM_REF:-}" ]; then
128+ # pre-commit --from-ref/--to-ref: use the provided base directly
129+ _PR_MERGE_BASE=$(git merge-base "$PRE_COMMIT_FROM_REF" HEAD 2>/dev/null || true)
130+ fi
131+ if [ -z "$_PR_MERGE_BASE" ]; then
132+ # Search all remotes (origin, upstream, etc.) for common trunk branch names.
133+ # This handles fork setups where origin=fork and upstream=main-repo.
134+ _CANDIDATE_BASE=""
135+ _CANDIDATE_DIST=0
136+ for _b in $(git for-each-ref --format='%(refname:short)' \
137+ 'refs/remotes/*/master' 'refs/remotes/*/main' \
138+ 'refs/remotes/*/develop' 'refs/remotes/*/dev' 2>/dev/null); do
139+ _mb=$(git merge-base HEAD "$_b" 2>/dev/null || true)
140+ [ -z "$_mb" ] && continue
141+ # Skip if merge-base is HEAD itself (branch is ahead of or equal to HEAD)
142+ [ "$_mb" = "$(git rev-parse HEAD 2>/dev/null)" ] && continue
143+ # Pick the candidate whose merge-base is furthest from HEAD
144+ # (most commits since divergence = most likely true PR base)
145+ _dist=$(git rev-list --count "$_mb"..HEAD 2>/dev/null || echo 0)
146+ if [ -z "$_CANDIDATE_BASE" ] || [ "$_dist" -gt "$_CANDIDATE_DIST" ]; then
147+ _CANDIDATE_BASE="$_mb"
148+ _CANDIDATE_DIST="$_dist"
149+ fi
150+ done
151+ _PR_MERGE_BASE="$_CANDIDATE_BASE"
152+ fi
153+ 
154+ # Only use PR-range mode when HEAD has diverged from the upstream base
155+ if [ -n "$_PR_MERGE_BASE" ] && [ "$_PR_MERGE_BASE" != "$(git rev-parse HEAD 2>/dev/null)" ]; then
156+ mkdir -p "$OAT_RESULT_DIR"
157+ _DONE_MARKER="$OAT_RESULT_DIR/.done_${_HEAD_SHA}"
158+ if [ -f "$_DONE_MARKER" ]; then
159+ echo "[OAT] [SKIP] Already scanned HEAD=${_HEAD_SHA}. Skipping duplicate invocation."
160+ exit 0
161+ fi
162+ else
163+ # HEAD == merge-base: on the upstream branch itself, no PR range
164+ _PR_MERGE_BASE=""
165+ fi
166+fi
167+ 
168+# ---------------------------------------------------------------------------
169+# 2b. Deduplicate parallel invocations within the same pre-commit run.
170+# Only the first instance that acquires the lock actually runs the scan;
171+# all others wait then exit 0.
172+# ---------------------------------------------------------------------------
173+if command -v flock >/dev/null 2>&1; then
174+ _OAT_SCAN_LOCK="${TMPDIR:-/tmp}/oat_scan_$(echo "$REPO_ROOT" | tr '/\\: ' '____').lock"
175+ exec 9>"$_OAT_SCAN_LOCK"
176+ if ! flock -n 9; then
177+ echo "[OAT] Another OAT scan instance is running. Waiting..."
178+ flock -w 120 9
179+ # Re-check done marker: if the scanning instance completed normally, skip.
180+ # If it exited abnormally (no marker), fall through and run the scan ourselves.
181+ if [ -n "$_DONE_MARKER" ] && [ -f "$_DONE_MARKER" ]; then
182+ echo "[OAT] Scan already completed by another instance. Skipping."
183+ exit 0
184+ fi
185+ echo "[OAT] Previous instance did not complete. Proceeding with scan..."
186+ # Fall through to run the scan below
187+ fi
188+ # This instance now owns the lock and will run the scan.
189+fi
190+ 
191+echo "[OAT] Running OAT scan (Python Edition) ??INCREMENTAL MODE"
74echo "[OAT] Project: $REPO_NAME"192echo "[OAT] Project: $REPO_NAME"
193+if [ -n "$_PR_MERGE_BASE" ]; then
194+ echo "[OAT] Mode: PR range ??scanning all files changed since merge-base"
195+else
196+ echo "[OAT] Mode: staged files ??scanning only currently staged files"
197+fi
75 198 
76# ---------------------------------------------------------------------------199# ---------------------------------------------------------------------------
77# 3. Collect staged files200# 3. Collect staged files
@@ -93,15 +216,19 @@ if [ $# -gt 0 ]; then
93 FILE_LIST="$FILE_LIST,$_abs"216 FILE_LIST="$FILE_LIST,$_abs"
94 fi217 fi
95 done218 done
96-else219+elif [ -n "$_PR_MERGE_BASE" ]; then
97- _STAGED=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null || true)220+ # PR range mode: collect ALL files changed since the branch diverged
221+ _STAGED=$(git diff --name-only --diff-filter=ACM "$_PR_MERGE_BASE" HEAD \
222+ 2>/dev/null || true)
98 if [ -z "$_STAGED" ]; then223 if [ -z "$_STAGED" ]; then
99- echo "[OAT] No staged files to check. Skipping."224+ echo "[OAT] No files changed in PR range. Skipping."
225+ [ -n "$_DONE_MARKER" ] && touch "$_DONE_MARKER" 2>/dev/null || true
100 exit 0226 exit 0
101 fi227 fi
102 FILE_COUNT=$(echo "$_STAGED" | wc -l | tr -d ' ')228 FILE_COUNT=$(echo "$_STAGED" | wc -l | tr -d ' ')
103 FILE_LIST=""229 FILE_LIST=""
104- for _f in $_STAGED; do230+ while IFS= read -r _f; do
231+ [ -z "$_f" ] && continue
105 case "$_f" in232 case "$_f" in
106 /*) _abs="$_f" ;;233 /*) _abs="$_f" ;;
107 *) _abs="$REPO_ROOT/$_f" ;;234 *) _abs="$REPO_ROOT/$_f" ;;
@@ -112,7 +239,33 @@ else
112 else239 else
113 FILE_LIST="$FILE_LIST,$_abs"240 FILE_LIST="$FILE_LIST,$_abs"
114 fi241 fi
115- done242+ done <<EOF
243+$_STAGED
244+EOF
245+else
246+ # Staged files mode: on upstream branch directly, scan only staged files
247+ _STAGED=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null || true)
248+ if [ -z "$_STAGED" ]; then
249+ echo "[OAT] No staged files to check. Skipping."
250+ exit 0
251+ fi
252+ FILE_COUNT=$(echo "$_STAGED" | wc -l | tr -d ' ')
253+ FILE_LIST=""
254+ while IFS= read -r _f; do
255+ [ -z "$_f" ] && continue
256+ case "$_f" in
257+ /*) _abs="$_f" ;;
258+ *) _abs="$REPO_ROOT/$_f" ;;
259+ esac
260+ [ -f "$_abs" ] || continue
261+ if [ -z "$FILE_LIST" ]; then
262+ FILE_LIST="$_abs"
263+ else
264+ FILE_LIST="$FILE_LIST,$_abs"
265+ fi
266+ done <<EOF
267+$_STAGED
268+EOF
116fi269fi
117 270 
118if [ -z "$FILE_LIST" ]; then271if [ -z "$FILE_LIST" ]; then
@@ -123,36 +276,40 @@ fi
123echo "[OAT] Checking $FILE_COUNT staged file(s)..."276echo "[OAT] Checking $FILE_COUNT staged file(s)..."
124 277 
125# ---------------------------------------------------------------------------278# ---------------------------------------------------------------------------
126-# 4. Ensure oat_reports/ exists and is in .gitignore279+# 4. Ensure report directories exist
127# ---------------------------------------------------------------------------280# ---------------------------------------------------------------------------
128mkdir -p "$OAT_REPORT_DIR"281mkdir -p "$OAT_REPORT_DIR"
129- 282+mkdir -p "$OAT_RESULT_DIR"
130-_GITIGNORE="$REPO_ROOT/.gitignore"
131-for _entry in "oat_reports" "log"; do
132- if ! grep -qE "^${_entry}/?$" "$_GITIGNORE" 2>/dev/null; then
133- printf "\n%s/\n" "$_entry" >> "$_GITIGNORE" 2>/dev/null || true
134- echo "[OAT] Added ${_entry}/ to .gitignore"
135- fi
136-done
137 283 
138# ---------------------------------------------------------------------------284# ---------------------------------------------------------------------------
139-# 5. Build oat command — use OAT.xml if present in repo root285+# 5. Resolve OAT.xml path
140# ---------------------------------------------------------------------------286# ---------------------------------------------------------------------------
141-_OAT_CMD="$_PYTHON -m oat -mode s -s $REPO_ROOT -r $OAT_REPORT_DIR -n $REPO_NAME -w 1 -f $FILE_LIST"
142- 
143_OAT_XML="$REPO_ROOT/OAT.xml"287_OAT_XML="$REPO_ROOT/OAT.xml"
288+if [ ! -f "$_OAT_XML" ] && [ -f "$REPO_ROOT/scripts/OAT.xml" ]; then
289+ _OAT_XML="$REPO_ROOT/scripts/OAT.xml"
290+fi
291+ 
292+# _OAT_CMD is kept for display purposes only (shown in error messages).
293+# Actual execution uses direct argument passing below to avoid eval word-splitting
294+# and command-injection risks when paths or filenames contain special characters.
144if [ -f "$_OAT_XML" ]; then295if [ -f "$_OAT_XML" ]; then
145- _OAT_CMD="$_OAT_CMD -oatconfig $_OAT_XML"296+ _OAT_CMD="\"$_PYTHON\" -m oat -mode s -s \"$REPO_ROOT\" -r \"$OAT_REPORT_DIR\" -n \"$REPO_NAME\" -w 1 -f \"$FILE_LIST\" -oatconfig \"$_OAT_XML\""
297+else
298+ _OAT_CMD="\"$_PYTHON\" -m oat -mode s -s \"$REPO_ROOT\" -r \"$OAT_REPORT_DIR\" -n \"$REPO_NAME\" -w 1 -f \"$FILE_LIST\""
146fi299fi
147 300 
148# ---------------------------------------------------------------------------301# ---------------------------------------------------------------------------
149-# 6. Run oat scan302+# 6. Run oat scan (direct argument passing � no eval)
150# ---------------------------------------------------------------------------303# ---------------------------------------------------------------------------
151echo ""304echo ""
152echo "[OAT] Running compliance scan..."305echo "[OAT] Running compliance scan..."
153 306 
154set +e307set +e
155-eval "$_OAT_CMD" >/dev/null 2>&1308+if [ -f "$_OAT_XML" ]; then
309+ "$_PYTHON" -m oat -mode s -s "$REPO_ROOT" -r "$OAT_REPORT_DIR" -n "$REPO_NAME" -w 1 -f "$FILE_LIST" -oatconfig "$_OAT_XML" >/dev/null 2>&1
310+else
311+ "$_PYTHON" -m oat -mode s -s "$REPO_ROOT" -r "$OAT_REPORT_DIR" -n "$REPO_NAME" -w 1 -f "$FILE_LIST" >/dev/null 2>&1
312+fi
156_OAT_RC=$?313_OAT_RC=$?
157set -e314set -e
158 315 
@@ -170,7 +327,7 @@ fi
170# Only: Invalid File Type + License Header Invalid (no copyright)327# Only: Invalid File Type + License Header Invalid (no copyright)
171# ---------------------------------------------------------------------------328# ---------------------------------------------------------------------------
172REPORT_FILE="$OAT_REPORT_DIR/PlainReport_${REPO_NAME}.txt"329REPORT_FILE="$OAT_REPORT_DIR/PlainReport_${REPO_NAME}.txt"
173-RESULT_FILE="$OAT_REPORT_DIR/result.txt"330+RESULT_FILE="$OAT_RESULT_DIR/result.txt"
174 331 
175# Section headers used as stop-boundaries when extracting sections332# Section headers used as stop-boundaries when extracting sections
176_ALL_HEADERS="Invalid File Type Total Count:|License Not Compatible Total Count:|License Header Invalid Total Count:|Copyright Header Invalid Total Count:|No License File Total Count:|No Readme.OpenSource Total Count:|No Readme Total Count:|Import Invalid Total Count:|Redundant License File Total Count:|Third Party Software Info Total Count:"333_ALL_HEADERS="Invalid File Type Total Count:|License Not Compatible Total Count:|License Header Invalid Total Count:|Copyright Header Invalid Total Count:|No License File Total Count:|No Readme.OpenSource Total Count:|No Readme Total Count:|Import Invalid Total Count:|Redundant License File Total Count:|Third Party Software Info Total Count:"
@@ -201,17 +358,29 @@ _extract_section() {
201 358 
202if [ ! -f "$REPORT_FILE" ]; then359if [ ! -f "$REPORT_FILE" ]; then
203 if [ "$_OAT_RC" -eq 0 ]; then360 if [ "$_OAT_RC" -eq 0 ]; then
361+ # oat exited cleanly with no report: all staged files were filtered out
204 echo "[OAT] [OK] All checks passed ($FILE_COUNT file(s) checked)."362 echo "[OAT] [OK] All checks passed ($FILE_COUNT file(s) checked)."
363+ [ -n "$_DONE_MARKER" ] && touch "$_DONE_MARKER" 2>/dev/null || true
364+ rm -rf "$OAT_REPORT_DIR"
205 exit 0365 exit 0
206 else366 else
207- echo "[OAT] [WARNING] Report not found: $REPORT_FILE"367+ # oat returned exit code 1 but produced no report �?possible disk issue
368+ # or oat internal error. Do not silently pass; block the commit.
369+ echo ""
370+ echo "[OAT] [ERROR] oat exited with code $_OAT_RC but no report was generated."
371+ echo "[OAT] This may indicate a disk error or an oat internal bug."
372+ echo "[OAT] To investigate, run manually:"
373+ echo " $_OAT_CMD"
374+ echo "[OAT] Blocking commit to prevent silent compliance bypass."
375+ echo ""
376+ rm -rf "$OAT_REPORT_DIR"
208 exit 1377 exit 1
209 fi378 fi
210fi379fi
211 380 
212-# Parse counts381+# Parse counts ??use || true to prevent set -e from triggering if grep finds no match
213-_INVALID_TYPE=$(grep "^Invalid File Type Total Count:" "$REPORT_FILE" | grep -oE '[0-9]+' | head -1)382+_INVALID_TYPE=$(grep "^Invalid File Type Total Count:" "$REPORT_FILE" | grep -oE '[0-9]+' | head -1 || true)
214-_LICENSE_INVALID=$(grep "^License Header Invalid Total Count:" "$REPORT_FILE" | grep -oE '[0-9]+' | head -1)383+_LICENSE_INVALID=$(grep "^License Header Invalid Total Count:" "$REPORT_FILE" | grep -oE '[0-9]+' | head -1 || true)
215_INVALID_TYPE=${_INVALID_TYPE:-0}384_INVALID_TYPE=${_INVALID_TYPE:-0}
216_LICENSE_INVALID=${_LICENSE_INVALID:-0}385_LICENSE_INVALID=${_LICENSE_INVALID:-0}
217 386 
@@ -240,11 +409,8 @@ _SECTION_LIC=$(_extract_section "$REPORT_FILE" "License Header Invalid Total Cou
240 echo "==================================="409 echo "==================================="
241} > "$RESULT_FILE"410} > "$RESULT_FILE"
242 411 
243-# Clean up full plain report (keep only result.txt)
244-rm -f "$REPORT_FILE"
245- 
246# ---------------------------------------------------------------------------412# ---------------------------------------------------------------------------
247-# 8. Block commit if issues found413+# 8. Block commit if issues found; always clean up temp dir
248# ---------------------------------------------------------------------------414# ---------------------------------------------------------------------------
249TOTAL_ISSUES=$(( _INVALID_TYPE + _LICENSE_INVALID ))415TOTAL_ISSUES=$(( _INVALID_TYPE + _LICENSE_INVALID ))
250 416 
@@ -258,12 +424,15 @@ if [ "$TOTAL_ISSUES" -gt 0 ]; then
258 echo " - Invalid File Type: $_INVALID_TYPE"424 echo " - Invalid File Type: $_INVALID_TYPE"
259 echo " - License Header Invalid: $_LICENSE_INVALID"425 echo " - License Header Invalid: $_LICENSE_INVALID"
260 echo ""426 echo ""
261- echo "[OAT] Details:"427+ echo "[OAT] Details (also saved to: $RESULT_FILE):"
262- echo " cat $RESULT_FILE"428+ echo "---"
429+ cat "$RESULT_FILE"
430+ echo "---"
263 echo ""431 echo ""
264 echo "Fix the issues and recommit, or skip with:"432 echo "Fix the issues and recommit, or skip with:"
265 echo " git commit --no-verify"433 echo " git commit --no-verify"
266 echo ""434 echo ""
435+ rm -rf "$OAT_REPORT_DIR"
267 exit 1436 exit 1
268fi437fi
269 438 
@@ -271,4 +440,10 @@ echo ""
271echo "[OAT] [OK] All checks passed ($FILE_COUNT file(s) checked)."440echo "[OAT] [OK] All checks passed ($FILE_COUNT file(s) checked)."
272echo "[OAT] Summary: cat $RESULT_FILE"441echo "[OAT] Summary: cat $RESULT_FILE"
273echo ""442echo ""
443+# Mark scan as done for CI range mode (prevents redundant re-runs on same PR head)
444+if [ -n "$_DONE_MARKER" ]; then
445+ touch "$_DONE_MARKER" 2>/dev/null || true
446+ echo "[OAT] Done-marker created: $_DONE_MARKER"
447+fi
448+rm -rf "$OAT_REPORT_DIR"
274exit 0449exit 0