#!/usr/bin/env bash ################################################################################# # This script is run by git before a commit is made. # # To use it, copy it to .git/hooks/pre-commit and make it executable. # # Alternatively, run the following command from the root of the repo: # # git config core.hooksPath .githooks # # # # FEATURES # # Updates the "updated" field in the front matter of .md files. # # Compresses PNG files with either oxipng or optipng if available. # # Runs subset_font if config.toml has been modified. # # # # Stops you from commiting: # # - a draft .md file # # - a file with a "TODO" # # - a JS file without a minified version # # - a minified JS file that isn't as small as it can be # # - a config.toml and theme.toml with different amounts of lines in [extra] # ################################################################################# # Function to exit the script with an error message. function error_exit() { echo "ERROR: $1" >&2 exit "${2:-1}" } # Function to extract the date from the front matter. function extract_date() { local file="$1" local field="$2" grep -m 1 "^$field =" "$file" | sed -e "s/$field = //" -e 's/ *$//' } # Function to check if the .md file is a draft. function is_draft() { local file="$1" awk '/^\+\+\+$/,/^\+\+\+$/ { if(/draft = true/) exit 0 } END { exit 1 }' "$file" } # Check if the file contains "TODO". function contains_todo() { local file="$1" grep -q "TODO" "$file" } # Check for changes outside of the front matter. function has_body_changes() { local file="$1" local in_front_matter=1 local triple_plus_count=0 diff_output=$(git diff --unified=999 --cached --output-indicator-new='%' --output-indicator-old='&' "$file") while read -r line; do if [[ "$line" =~ ^\+\+\+$ ]]; then triple_plus_count=$((triple_plus_count + 1)) if [[ $triple_plus_count -eq 2 ]]; then in_front_matter=0 fi elif [[ $in_front_matter -eq 0 ]]; then if [[ "$line" =~ ^[\%\&] ]]; then return 0 fi fi done <<< "$diff_output" return 1 } # Function to update the social media card for a post or section. function generate_and_commit_card { local file=$1 social_media_card=$(social-cards-zola -o static/img/social_cards -b http://127.0.0.1:1111 -u -p -i "$file") || { echo "Failed to update social media card for $file" exit 1 } git add "$social_media_card" || { echo "Failed to add social media card $social_media_card" exit 1 } git add "$file" || { echo "Failed to add $file" exit 1 } } export -f generate_and_commit_card function has_minified_version() { local file="$1" local extension="${file##*.}" local minified_file="${file%.*}.min.$extension" [ -f "$minified_file" ] } function is_minified() { local file="$1" # Check if terser and uglifyjs are installed. if ! command -v terser &> /dev/null || ! command -v uglifyjs &> /dev/null; then echo "Either terser or uglifyjs is not installed. Skipping minification check." return 0 fi # Original file size. local original_size=$(wc -c < "$file") # File size after compression with terser. local terser_size=$(terser --compress --mangle -- "$file" | wc -c) # File size after compression with uglifyjs. local uglifyjs_size=$(uglifyjs --compress --mangle -- "$file" | wc -c) # Check if the file is already as small as or smaller than both minified versions. if (( original_size <= terser_size && original_size <= uglifyjs_size )); then return 0 fi # If the file isn't as small as it can be, suggest the better compressor in the error message if (( terser_size < uglifyjs_size )); then error_exit "Minified JS file $file isn't as small as it can be! Try using terser for better compression." else error_exit "Minified JS file $file isn't as small as it can be! Try using uglifyjs for better compression." fi } # Check if the script is being run from the root of the repo. if [[ ! $(git rev-parse --show-toplevel) == $(pwd) ]]; then error_exit "This script must be run from the root of the repo." fi # Check if oxipng is installed. png_compressor="" if command -v oxipng &> /dev/null; then png_compressor="oxipng -o max" elif command -v optipng &> /dev/null; then png_compressor="optipng -o 7" fi ################################################################## # Compress PNG files with either oxipng or optipng if available. # # Update the "updated" field in the front matter of .md files. # # https://osc.garden/blog/zola-date-git-hook/ # # Ensure the [extra] section from config.toml and theme.toml # # have the same amount of lines. # # Ensure JavaScript files are minified. # ################################################################## # Get the newly added and modified files, but not deleted files. mapfile -t all_changed_files < <(git diff --cached --name-only --diff-filter=d) script_name=$(basename "$0") # Loop through all newly added or modified files. for file in "${all_changed_files[@]}"; do file_name=$(basename "$file") # Ignore this script and the changelog. if [[ "$file_name" == "$script_name" ]] || [[ "$file_name" == "CHANGELOG.md" ]]; then continue fi # If the file is a PNG and png_compressor is set, compress it and add it to the commit. if [[ "$file" == *.png ]] && [[ -n "$png_compressor" ]]; then $png_compressor "$file" || error_exit "Failed to compress PNG file $file" git add --force "$file" || error_exit "Failed to add compressed PNG file $file" continue fi # If the file contains "TODO", abort the commit. if contains_todo "$file"; then error_exit "File $file contains TODO! Remove or complete the TODO before committing." fi # If the file is a JS file and it doesn't have a minified version, abort the commit. if [[ "$file" == *.js ]] && [[ "$file" != *.min.js ]] && ! has_minified_version "$file"; then error_exit "JS file $file doesn't have a minified version!" fi # If the file is a minified JS file and it isn't as small as it can be, abort the commit. # Error message shows which minifier is best for the file. if [[ "$file" == *.min.js ]]; then is_minified "$file" fi # Ensure the [extra] section from config.toml and theme.toml have the same amount of lines. if [[ "$file" == "config.toml" ]] || [[ "$file" == "theme.toml" ]]; then # Get the line number where [extra] starts in config.toml. extra_line_config=$(grep -n "^\[extra\]$" "config.toml" | cut -d: -f1) # Get the line number where [extra] starts in theme.toml. extra_line_theme=$(grep -n "^\[extra\]$" "theme.toml" | cut -d: -f1) # Get the number of lines in the [extra] section in config.toml. extra_lines_config=$(tail -n +"$extra_line_config" "config.toml" | wc -l) # Get the number of lines in the [extra] section in theme.toml. extra_lines_theme=$(tail -n +"$extra_line_theme" "theme.toml" | wc -l) # Abort the commit if the number of lines in the [extra] section don't match. if [[ "$extra_lines_config" -ne "$extra_lines_theme" ]]; then error_exit "The [extra] section in config.toml and theme.toml don't have the same amount of lines!" fi fi done # Get the modified .md to update the "updated" field in the front matter. modified_md_files=$(git diff --cached --name-only --diff-filter=M | grep -E '\.md$' | grep -v '_index.md$') # Loop through each modified .md file. for file in $modified_md_files; do echo $file # If the file is an .md file and it's a draft, abort the commit. if is_draft "$file"; then error_exit "Draft file $file is being committed!" fi # If changes are only in the front matter, skip the file. if ! has_body_changes "$file"; then continue fi # Modify the "updated" date, if necessary. # Get the last modified date from the filesystem. last_modified_date=$(date -r "$file" +'%Y-%m-%d') # Extract the "date" field from the front matter. date_value=$(extract_date "$file" "date") # Skip the file if the last modified date is the same as the "date" field. if [[ "$last_modified_date" == "$date_value" ]]; then continue fi # Update the "updated" field with the last modified date. # If the "updated" field doesn't exist, create it below the "date" field. if ! awk -v date_line="$last_modified_date" 'BEGIN{FS=OFS=" = "; first = 1} { if (/^date =/ && first) { print; getline; if (!/^updated =/) print "updated" OFS date_line; first=0; } if (/^updated =/ && !first) gsub(/[^ ]*$/, date_line, $2); print; }' "$file" > "${file}.tmp" then error_exit "Failed to process $file with AWK" fi mv "${file}.tmp" "$file" || error_exit "Failed to overwrite $file with updated content" # Stage the changes. git add "$file" done # Use `social-cards-zola` to create/update the social media card for Markdown files. # See https://osc.garden/blog/automating-social-media-cards-zola/ for context. # Use parallel to create the social media cards in parallel and commit them. # echo "$modified_md_files" | parallel -j 8 generate_and_commit_card ######################################################### # Run subset_font if config.toml has been modified. # # https://welpo.github.io/tabi/blog/custom-font-subset/ # ######################################################### if git diff --cached --name-only | grep -q "config.toml"; then echo "config.toml modified. Attempting to run subset_font…" # Check if subset_font is available and exit early if not. if ! command -v subset_font &> /dev/null; then echo "subset_font command not found. Skipping this step." exit 0 fi # Call the subset_font script. ~/bin/subset_font -c config.toml -f static/fonts/Inter4.woff2 -o static/ # Add the generated subset.css file to the commit. git add static/custom_subset.css fi