#!/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. # # Creates/updates the social media card for .md files. # # Aborts the commit if a draft .md file or a file with "TODO" is being committed. # ################################################################################### # 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 # 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" grep -q "draft = true" "$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=$(./static/code/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 # Get the modified .md to update the "updated" field in the front matter. mapfile -t modified_md_files < <(git diff --cached --name-only --diff-filter=M | grep -Ei '\.md$' | grep -v '_index.md$') # Loop through each modified .md file. for file in "${modified_md_files[@]}"; do # If changes are only in the front matter, skip the file. if ! has_body_changes "$file"; then continue fi # 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 # 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/ # # Interrupt the commit if a draft .md file is being committed. # # Interrupt the commit if a file with "TODO" is being committed. # # Create/update the social media card for posts and sections. # # For more info about the social media cards part, see: # # https://osc.garden/blog/automating-social-media-cards-zola # ################################################################## # Get the newly added and modified files, but not deleted files. mapfile -t all_changed_files < <(git diff --cached --name-only --diff-filter=d) # Loop through each newly added or modified .md or .png file for PNG optimization and draft checking. for file in "${all_changed_files[@]}"; do # Ignore this script. if [[ "$file" == "$0" ]]; 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 "$file" || error_exit "Failed to add compressed PNG file $file" continue fi # If the file is an .md file and it's a draft, abort the commit. if [[ "$file" == *.md ]]; then if is_draft "$file"; then error_exit "Draft file $file is being committed!" fi fi 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. changed_md_files=$(echo "$all_changed_files" | grep '\.md$') echo "Changed Markdown files: $changed_md_files" # Use parallel to create the social media cards in parallel and commit them. if [[ -n "$changed_md_files" ]]; then echo "$changed_md_files" | parallel -j 8 generate_and_commit_card fi