/ scripts / add-license.sh
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 "$@"