|
| 1 | +#!/bin/bash |
| 2 | +# This file was generated using AI assistance (Cursor AI) and reviewed by the maintainers. |
| 3 | +# |
| 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. |
| 6 | +# |
| 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 |
| 17 | +# |
| 18 | +# 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 |
| 24 | +# |
| 25 | +# 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.) |
| 29 | +set -u |
| 30 | + |
| 31 | +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 32 | +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" |
| 33 | +cd "$REPO_ROOT" |
| 34 | + |
| 35 | +VERBOSE=false |
| 36 | +OUTPUT_MODE="text" |
| 37 | +COMMIT_RANGE="" |
| 38 | +for arg in "$@"; do |
| 39 | + case "$arg" in |
| 40 | + --verbose|-v) VERBOSE=true ;; |
| 41 | + --pr-comment) OUTPUT_MODE="pr-comment" ;; |
| 42 | + --list) OUTPUT_MODE="list" ;; |
| 43 | + *..*) COMMIT_RANGE="$arg" ;; |
| 44 | + esac |
| 45 | +done |
| 46 | + |
| 47 | +# --- Locate the last rebase commit --- |
| 48 | +if [ -z "$COMMIT_RANGE" ]; then |
| 49 | + REBASE_COMMIT=$(git log --format='%H' --grep="^Rebase against the upstream" -1) |
| 50 | + 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'." |
| 56 | + exit 2 |
| 57 | + fi |
| 58 | + REBASE_MSG=$(git log -1 --format='%s' "$REBASE_COMMIT") |
| 59 | + COMMIT_RANGE="${REBASE_COMMIT}..HEAD" |
| 60 | +fi |
| 61 | + |
| 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 |
| 72 | + |
| 73 | +# --- Build the set of files covered by rebase rules --- |
| 74 | +COVERED_LIST=$(mktemp) |
| 75 | +trap 'rm -f "$COVERED_LIST"' EXIT |
| 76 | + |
| 77 | +# 1. Parse resolve_conflicts() elif chain for explicit file paths. |
| 78 | +while IFS= read -r line; do |
| 79 | + if [[ "$line" =~ \[\[.*==.*\"([^\"]+)\".*\]\] ]]; then |
| 80 | + echo "${BASH_REMATCH[1]}" |
| 81 | + fi |
| 82 | +done < <(sed -n '/^resolve_conflicts()/,/^}/p' rebase.sh) >> "$COVERED_LIST" |
| 83 | + |
| 84 | +# 2. .rebase/replace/ rules |
| 85 | +find .rebase/replace -name '*.json' -type f 2>/dev/null | while IFS= read -r rule_file; do |
| 86 | + code_path="${rule_file#.rebase/replace/}" |
| 87 | + code_path="${code_path%.json}" |
| 88 | + if [[ "$code_path" != code/* ]]; then |
| 89 | + code_path="code/$code_path" |
| 90 | + fi |
| 91 | + echo "$code_path" |
| 92 | +done >> "$COVERED_LIST" |
| 93 | + |
| 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" |
| 103 | + |
| 104 | +sort -u -o "$COVERED_LIST" "$COVERED_LIST" |
| 105 | + |
| 106 | +# --- Check changed files --- |
| 107 | +UNCOVERED=() |
| 108 | +COVERED_COUNT=0 |
| 109 | +SKIPPED_COUNT=0 |
| 110 | + |
| 111 | +while IFS= read -r file_path; do |
| 112 | + # Skip package-lock.json (handled by resolve_package_lock during rebase) |
| 113 | + 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 ;; |
| 121 | + esac |
| 122 | + |
| 123 | + if grep -qxF "$file_path" "$COVERED_LIST"; then |
| 124 | + ((COVERED_COUNT++)) |
| 125 | + continue |
| 126 | + fi |
| 127 | + |
| 128 | + UNCOVERED+=("$file_path") |
| 129 | +done < <(git diff --name-only "$COMMIT_RANGE" -- code/) |
| 130 | + |
| 131 | +# --- Output --- |
| 132 | + |
| 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 |
| 139 | +fi |
| 140 | + |
| 141 | +# --pr-comment mode: markdown for a GitHub PR comment |
| 142 | +if [ "$OUTPUT_MODE" = "pr-comment" ]; then |
| 143 | + if [ ${#UNCOVERED[@]} -eq 0 ]; then |
| 144 | + exit 0 |
| 145 | + 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 |
| 165 | + |
| 166 | +# Default text mode |
| 167 | +if [ "$VERBOSE" = true ]; then |
| 168 | + echo "Covered: $COVERED_COUNT | Skipped: $SKIPPED_COUNT | Uncovered: ${#UNCOVERED[@]}" |
| 169 | + echo "" |
| 170 | + echo "Rebase rules on file:" |
| 171 | + sed 's/^/ /' "$COVERED_LIST" |
| 172 | + echo "" |
| 173 | +fi |
| 174 | + |
| 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" |
| 178 | + exit 0 |
| 179 | +fi |
| 180 | + |
| 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." |
| 188 | +echo "" |
| 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" |
| 193 | +exit 1 |
0 commit comments