11#! /bin/bash
22# This file was generated using AI assistance (Cursor AI) and reviewed by the maintainers.
33#
4- # Detect files under code/ changed since the last rebase that have no rebase
5- # rule. Such modifications would be silently lost on the next upstream rebase.
4+ # Detect files under code/ changed in a PR whose modifications are NOT fully
5+ # protected by rebase rules. Two categories:
6+ # - "Missing rule" — no rebase rule exists for the file at all
7+ # - "Incomplete rule" — a rule exists but doesn't reproduce the current content
68#
7- # How it works:
8- # 1. Finds the last rebase commit (message starts with "Rebase against the upstream")
9- # 2. Lists files in code/ changed since that commit
10- # 3. Filters out Che-only additions (che-* extensions, che/ subdirs) and lockfiles
11- # 4. For remaining files, checks whether a rebase rule exists in:
12- # - resolve_conflicts() elif chain in rebase.sh
13- # - .rebase/replace/ (from→by rules)
14- # - .rebase/add/ (JSON fragments merged in)
15- # - .rebase/override/ (JSON overrides)
16- # 5. Reports any uncovered files
9+ # For "incomplete" detection the script runs the actual rebase handler against
10+ # upstream content and diffs the result against the working tree, reusing the
11+ # test infrastructure from .claude/skills/test-rebase-rules/.
1712#
1813# Usage:
19- # bash check-unprotected-changes.sh # default: changes since last rebase
20- # bash check-unprotected-changes.sh <base>..<head> # explicit commit range
21- # bash check-unprotected-changes.sh --verbose # show covered files too
22- # bash check-unprotected-changes.sh --pr-comment # output as GitHub PR comment (markdown)
23- # bash check-unprotected-changes.sh --list # output bare file paths only
14+ # bash check-unprotected-changes.sh --pr-comment <base>..<head>
15+ #
16+ # Prerequisites (for incomplete-rule detection):
17+ # - upstream-code remote must be fetched for the current upstream version
2418#
2519# Exit codes:
26- # 0 — All modifications are covered by rebase rules (or are Che-only additions)
27- # 1 — Unprotected modifications found
28- # 2 — Error (can't find rebase commit, etc.)
20+ # 0 — All modifications are fully covered
21+ # 1 — Problems found
22+ # 2 — Error
2923set -u
3024
3125SCRIPT_DIR=" $( cd " $( dirname " ${BASH_SOURCE[0]} " ) " && pwd) "
3226REPO_ROOT=" $( cd " $SCRIPT_DIR /../.." && pwd) "
3327cd " $REPO_ROOT "
3428
35- VERBOSE=false
3629OUTPUT_MODE=" text"
3730COMMIT_RANGE=" "
3831for arg in " $@ " ; do
3932 case " $arg " in
40- --verbose|-v) VERBOSE=true ;;
4133 --pr-comment) OUTPUT_MODE=" pr-comment" ;;
42- --list) OUTPUT_MODE=" list" ;;
4334 * ..* ) COMMIT_RANGE=" $arg " ;;
4435 esac
4536done
4637
47- # --- Locate the last rebase commit ---
4838if [ -z " $COMMIT_RANGE " ]; then
4939 REBASE_COMMIT=$( git log --format=' %H' --grep=" ^Rebase against the upstream" -1)
5040 if [ -z " $REBASE_COMMIT " ]; then
51- if [ " $OUTPUT_MODE " = " list" ]; then
52- exit 2
53- fi
54- echo " ERROR: Could not find a rebase commit in the history."
55- echo " Expected a commit whose message starts with 'Rebase against the upstream'."
41+ echo " ERROR: Could not find a rebase commit and no commit range provided." >&2
5642 exit 2
5743 fi
58- REBASE_MSG=$( git log -1 --format=' %s' " $REBASE_COMMIT " )
5944 COMMIT_RANGE=" ${REBASE_COMMIT} ..HEAD"
6045fi
6146
62- if [ " $OUTPUT_MODE " = " text" ]; then
63- if [ -n " ${REBASE_MSG:- } " ]; then
64- echo " === Unprotected Changes Check ==="
65- echo " Base: $REBASE_COMMIT (${REBASE_MSG} )"
66- else
67- echo " === Unprotected Changes Check ==="
68- echo " Range: $COMMIT_RANGE "
69- fi
70- echo " "
71- fi
47+ UPSTREAM_VERSION=$( grep ' ^CURRENT_UPSTREAM_VERSION=' rebase.sh | head -1 | sed ' s/.*="\(.*\)"/\1/' )
48+
49+ HANDLER_SCRIPT=" $REPO_ROOT /.claude/skills/test-rebase-rules/test-rebase-handler.sh"
50+ RUN_ALL_TESTS=" $REPO_ROOT /.claude/skills/test-rebase-rules/run-all-tests.sh"
7251
7352# --- Build the set of files covered by rebase rules ---
7453COVERED_LIST=$( mktemp)
7554trap ' rm -f "$COVERED_LIST"' EXIT
7655
77- # 1. Parse resolve_conflicts() elif chain for explicit file paths.
7856while IFS= read -r line; do
7957 if [[ " $line " =~ \[\[ .* == .* \" ([^\" ]+)\" .* \]\] ]]; then
8058 echo " ${BASH_REMATCH[1]} "
8159 fi
8260done < <( sed -n ' /^resolve_conflicts()/,/^}/p' rebase.sh) >> " $COVERED_LIST "
8361
84- # 2. .rebase/replace/ rules
8562find .rebase/replace -name ' *.json' -type f 2> /dev/null | while IFS= read -r rule_file; do
8663 code_path=" ${rule_file# .rebase/ replace/ } "
8764 code_path=" ${code_path% .json} "
@@ -91,103 +68,109 @@ find .rebase/replace -name '*.json' -type f 2>/dev/null | while IFS= read -r rul
9168 echo " $code_path "
9269done >> " $COVERED_LIST "
9370
94- # 3. .rebase/add/ rules
95- find .rebase/add -type f 2> /dev/null | while IFS= read -r rule_file; do
96- echo " ${rule_file# .rebase/ add/ } "
97- done >> " $COVERED_LIST "
98-
99- # 4. .rebase/override/ rules
100- find .rebase/override -type f 2> /dev/null | while IFS= read -r rule_file; do
101- echo " ${rule_file# .rebase/ override/ } "
102- done >> " $COVERED_LIST "
71+ find .rebase/add -type f 2> /dev/null | while IFS= read -r f; do echo " ${f# .rebase/ add/ } " ; done >> " $COVERED_LIST "
72+ find .rebase/override -type f 2> /dev/null | while IFS= read -r f; do echo " ${f# .rebase/ override/ } " ; done >> " $COVERED_LIST "
10373
10474sort -u -o " $COVERED_LIST " " $COVERED_LIST "
10575
106- # --- Check changed files ---
107- UNCOVERED=()
108- COVERED_COUNT=0
109- SKIPPED_COUNT=0
76+ # --- Collect changed files and classify ---
77+ MISSING_RULE=()
78+ TO_TEST=()
11079
11180while IFS= read -r file_path; do
112- # Skip package-lock.json (handled by resolve_package_lock during rebase)
11381 case " $file_path " in
114- * /package-lock.json) (( SKIPPED_COUNT++ )) ; continue ;;
115- esac
116-
117- # Skip Che-only additions — these don't exist in upstream and won't conflict
118- case " $file_path " in
119- code/extensions/che-* ) (( SKIPPED_COUNT++ )) ; continue ;;
120- * /che/* .ts|* /che/* .js) (( SKIPPED_COUNT++ )) ; continue ;;
82+ * /package-lock.json) continue ;;
83+ code/extensions/che-* ) continue ;;
84+ * /che/* .ts|* /che/* .js) continue ;;
12185 esac
12286
12387 if grep -qxF " $file_path " " $COVERED_LIST " ; then
124- (( COVERED_COUNT++ ))
125- continue
88+ TO_TEST+=(" $file_path " )
89+ else
90+ MISSING_RULE+=(" $file_path " )
12691 fi
127-
128- UNCOVERED+=(" $file_path " )
12992done < <( git diff --name-only " $COMMIT_RANGE " -- code/)
13093
131- # --- Output ---
94+ # --- Test files that have rules: are rules complete? ---
95+ INCOMPLETE_RULE=()
96+ HAS_UPSTREAM=false
97+ if [ -n " $UPSTREAM_VERSION " ]; then
98+ git rev-parse " upstream-code/$UPSTREAM_VERSION " > /dev/null 2>&1 && HAS_UPSTREAM=true
99+ fi
100+
101+ if [ " $HAS_UPSTREAM " = true ] && [ ${# TO_TEST[@]} -gt 0 ] && [ -f " $RUN_ALL_TESTS " ]; then
102+ TEST_OUTPUT=$( bash " $RUN_ALL_TESTS " " ${TO_TEST[@]} " 2>&1 ) || true
132103
133- # --list mode: bare file paths, nothing else
134- if [ " $OUTPUT_MODE " = " list " ]; then
135- for f in " ${UNCOVERED[@]+ ${UNCOVERED[@]} } " ; do
136- echo " $f "
137- done
138- [ ${ # UNCOVERED[@]} -eq 0 ] && exit 0 || exit 1
104+ while IFS= read -r line ; do
105+ if [[ " $line " =~ ^ \|\ \` ([^ \` ]+) \`\ \| ] ]; then
106+ failed_file= " ${BASH_REMATCH[1]} "
107+ INCOMPLETE_RULE+=( " $failed_file " )
108+ fi
109+ done < <( echo " $TEST_OUTPUT " | sed -n ' /^### Failures/,/^### /p ' | grep ' ^| ` ' )
139110fi
140111
141- # --pr-comment mode: markdown for a GitHub PR comment
112+ # --- Output ---
113+ TOTAL_ISSUES=$(( ${# MISSING_RULE[@]} + ${# INCOMPLETE_RULE[@]} ))
114+
142115if [ " $OUTPUT_MODE " = " pr-comment" ]; then
143- if [ ${ # UNCOVERED[@]} -eq 0 ]; then
116+ if [ $TOTAL_ISSUES -eq 0 ]; then
144117 exit 0
145118 fi
146- cat << 'HEADER '
147- <!-- rebase-rules-check -->
148- ### Missing Rebase Rules
149-
150- The following files were modified in this PR but do not have corresponding rebase rules.
151- Without these rules, the changes **will be lost** during the next upstream rebase.
152-
153- | # | File |
154- |---|------|
155- HEADER
156- i=1
157- for f in " ${UNCOVERED[@]} " ; do
158- echo " | $i | \` $f \` |"
159- (( i++ ))
160- done
161- echo " "
162- echo " Comment \` /add-rebase-rules\` on this PR to add the missing rules automatically."
163- exit 1
164- fi
165119
166- # Default text mode
167- if [ " $VERBOSE " = true ]; then
168- echo " Covered: $COVERED_COUNT | Skipped: $SKIPPED_COUNT | Uncovered: ${# UNCOVERED[@]} "
120+ echo " <!-- rebase-rules-check -->"
121+ echo " ### Rebase Rules Check"
169122 echo " "
170- echo " Rebase rules on file: "
171- sed ' s/^/ / ' " $COVERED_LIST "
123+ echo " The following files were modified in this PR but their changes are **not fully protected** by rebase rules. "
124+ echo " Without proper rules, these changes **will be lost** during the next upstream rebase. "
172125 echo " "
126+
127+ if [ ${# MISSING_RULE[@]} -gt 0 ]; then
128+ echo " #### Missing rules (no rebase rule exists)"
129+ echo " "
130+ echo " | # | File |"
131+ echo " |---|------|"
132+ i=1
133+ for f in " ${MISSING_RULE[@]} " ; do
134+ echo " | $i | \` $f \` |"
135+ (( i++ ))
136+ done
137+ echo " "
138+ fi
139+
140+ if [ ${# INCOMPLETE_RULE[@]} -gt 0 ]; then
141+ echo " #### Incomplete rules (rule exists but doesn't cover new changes)"
142+ echo " "
143+ echo " | # | File |"
144+ echo " |---|------|"
145+ i=1
146+ for f in " ${INCOMPLETE_RULE[@]} " ; do
147+ echo " | $i | \` $f \` |"
148+ (( i++ ))
149+ done
150+ echo " "
151+ fi
152+
153+ echo " Comment \` /add-rebase-rules\` on this PR to add or update the rules automatically."
154+ exit 1
173155fi
174156
175- if [ ${ # UNCOVERED[@]} -eq 0 ] ; then
176- echo " All modified files in code/ are covered by rebase rules (or are Che-only additions). "
177- echo " Covered: $COVERED_COUNT Skipped (lockfiles/Che-only): $SKIPPED_COUNT "
157+ # Default text mode
158+ if [ $TOTAL_ISSUES -eq 0 ] ; then
159+ echo " All modified files in code/ are fully covered by rebase rules. "
178160 exit 0
179161fi
180162
181- echo " **Found ${# UNCOVERED[@]} file(s) modified since the last rebase without rebase rules:**"
182- echo " "
183- for f in " ${UNCOVERED[@]} " ; do
184- echo " - $f "
185- done
186- echo " "
187- echo " These changes will be lost on the next upstream rebase unless a rule is added."
163+ echo " Found $TOTAL_ISSUES file(s) with rebase rule problems:"
188164echo " "
189- echo " To fix, for each file above either:"
190- echo " 1. Add a .rebase/replace/<path>.json with {\" from\" : ..., \" by\" : ...} rules"
191- echo " 2. Add a .rebase/add/<path> or .rebase/override/<path> JSON fragment"
192- echo " 3. Add an elif branch in resolve_conflicts() in rebase.sh"
165+ if [ ${# MISSING_RULE[@]} -gt 0 ]; then
166+ echo " Missing rules:"
167+ for f in " ${MISSING_RULE[@]} " ; do echo " - $f " ; done
168+ echo " "
169+ fi
170+ if [ ${# INCOMPLETE_RULE[@]} -gt 0 ]; then
171+ echo " Incomplete rules:"
172+ for f in " ${INCOMPLETE_RULE[@]} " ; do echo " - $f " ; done
173+ echo " "
174+ fi
175+ echo " Comment /add-rebase-rules on this PR to fix automatically."
193176exit 1
0 commit comments