add-license.sh
1 #!/bin/bash 2 3 # Copyright 2025 Alibaba Group Holding Ltd. 4 # 5 # Licensed under the Apache License, Version 2.0 (the "License"); 6 # you may not use this file except in compliance with the License. 7 # You may obtain a copy of the License at 8 # 9 # http://www.apache.org/licenses/LICENSE-2.0 10 # 11 # Unless required by applicable law or agreed to in writing, software 12 # distributed under the License is distributed on an "AS IS" BASIS, 13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 # See the License for the specific language governing permissions and 15 # limitations under the License. 16 17 # Add Apache 2.0 license headers to source files that are missing them. 18 # Usage: run from repo root: ./scripts/add-license.sh 19 20 set -euo pipefail 21 22 LICENSE_YEAR=$(date +%Y) 23 LICENSE_OWNER="Alibaba Group Holding Ltd." 24 LICENSE_MARKER_REGEX="Copyright [0-9]{4} ${LICENSE_OWNER// / }" 25 LICENSE_TEXT_TEMPLATE=$( 26 cat <<'EOF' 27 Copyright __YEAR__ Alibaba Group Holding Ltd. 28 29 Licensed under the Apache License, Version 2.0 (the "License"); 30 you may not use this file except in compliance with the License. 31 You may obtain a copy of the License at 32 33 http://www.apache.org/licenses/LICENSE-2.0 34 35 Unless required by applicable law or agreed to in writing, software 36 distributed under the License is distributed on an "AS IS" BASIS, 37 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 38 See the License for the specific language governing permissions and 39 limitations under the License. 40 EOF 41 ) 42 43 # Basenames to include regardless of extension. 44 INCLUDE_BASENAMES=("Dockerfile") 45 46 # Paths to ignore. 47 IGNORES=( 48 ".git" 49 "node_modules" 50 ".venv" 51 "venv" 52 "dist" 53 "build" 54 "__pycache__" 55 "LICENSE" 56 "NOTICE" 57 "README.md" 58 "README_zh.md" 59 "scripts/spec-doc/index.html" 60 ) 61 62 has_license() { 63 local file="$1" 64 head -n 40 "$file" | grep -Eq "$LICENSE_MARKER_REGEX" 65 } 66 67 comment_header() { 68 local style="$1" 69 local text="$2" 70 case "$style" in 71 "line:#") 72 printf '%s\n' "$text" | sed 's/^/# /' 73 ;; 74 "line://") 75 printf '%s\n' "$text" | sed 's:^:// :' 76 ;; 77 "block:html") 78 printf "<!--\n%s\n-->\n" "$text" 79 ;; 80 "block:css") 81 printf "/*\n%s\n*/\n" "$text" 82 ;; 83 *) 84 return 1 85 ;; 86 esac 87 } 88 89 should_ignore_path() { 90 local file="$1" 91 for ignore in "${IGNORES[@]}"; do 92 if [[ "$file" == "$ignore" || "$file" == "$ignore"/* ]]; then 93 return 0 94 fi 95 done 96 return 1 97 } 98 99 is_k8s_mock_go() { 100 local file="${1-}" 101 [[ -z "$file" ]] && return 1 102 # Skip any Go mocks under kubernetes/internal: 103 # - filenames ending with _mock.go 104 # - any file under a /mock/ directory 105 if [[ "$file" != kubernetes/internal/* ]]; then 106 return 1 107 fi 108 if [[ "$file" == *"_mock.go" ]]; then 109 return 0 110 fi 111 if [[ "$file" == */mock/*.go ]]; then 112 return 0 113 fi 114 return 1 115 } 116 117 is_generated_go() { 118 local file="${1-}" 119 [[ -z "$file" ]] && return 1 120 [[ "${file##*.}" != "go" ]] && return 1 121 122 local base 123 base="$(basename "$file")" 124 case "$base" in 125 zz_generated.*|*.pb.go|*_pb.go|*_gen.go|*_generated.go) 126 return 0 127 ;; 128 esac 129 130 if head -n 5 "$file" | grep -qi "code generated"; then 131 return 0 132 fi 133 134 return 1 135 } 136 137 style_for_file() { 138 local file="${1-}" 139 [[ -z "$file" ]] && { echo ""; return; } 140 local base ext 141 base="$(basename "$file")" 142 ext="${file##*.}" 143 144 case "$ext" in 145 sh|py|toml|tf|sql) echo "line:#"; return ;; 146 go|java|kt|kts|ts|tsx|js|jsx|mjs|cjs|mts|cts) echo "line://"; return ;; 147 css) echo "block:css"; return ;; 148 html) echo "block:html"; return ;; 149 esac 150 151 for b in "${INCLUDE_BASENAMES[@]}"; do 152 if [[ "$base" == "$b" ]]; then 153 echo "line:#" 154 return 155 fi 156 done 157 158 echo "" 159 } 160 161 process_file() { 162 local file="${1-}" 163 [[ -z "$file" ]] && return 164 local style 165 166 if should_ignore_path "$file"; then 167 return 168 fi 169 170 if is_k8s_mock_go "$file"; then 171 return 172 fi 173 174 style="$(style_for_file "$file")" 175 [[ -z "$style" ]] && return 176 177 if is_generated_go "$file"; then 178 return 179 fi 180 181 if has_license "$file"; then 182 return 183 fi 184 185 local license_text header 186 license_text="${LICENSE_TEXT_TEMPLATE/__YEAR__/$LICENSE_YEAR}" 187 header="$(comment_header "$style" "$license_text")" 188 189 # Respect shebang: insert after the first line if it starts with #! 190 if head -n1 "$file" | grep -q "^#!"; then 191 local first rest 192 first="$(head -n1 "$file")" 193 rest="$(tail -n +2 "$file")" 194 printf '%s\n\n%s\n\n%s' "$first" "$header" "$rest" >"$file" 195 # Place before DOCTYPE for HTML to avoid breaking rendering. 196 elif head -n1 "$file" | grep -qi "^<!doctype"; then 197 local body 198 body="$(cat "$file")" 199 printf '%s\n\n%s' "$header" "$body" >"$file" 200 else 201 local body 202 body="$(cat "$file")" 203 printf '%s\n\n%s' "$header" "$body" >"$file" 204 fi 205 echo "Added license: $file" 206 } 207 208 main() { 209 local files 210 if [[ "$#" -gt 0 ]]; then 211 IFS=$'\n' read -r -d '' -a files < <(git ls-files -- "$@" && printf '\0') 212 else 213 IFS=$'\n' read -r -d '' -a files < <(git ls-files && printf '\0') 214 fi 215 for f in "${files[@]}"; do 216 process_file "$f" 217 done 218 } 219 220 main "$@"