commit 223b88afd7cf94abafd08642e8f0fae8dfdc05a3 Author: Darren 'Tadgy' Austin Date: Sat Dec 16 20:01:26 2023 +0000 Add gitattributesdb git hook script. diff --git a/gitattributsdb b/gitattributsdb new file mode 100755 index 0000000..1e0f3e8 --- /dev/null +++ b/gitattributsdb @@ -0,0 +1,192 @@ +#!/usr/bin/env bash +# Version: 0.0.2 +# Copyright (c) 2023: +# Darren 'Tadgy' Austin +# Licensed under the terms of the GNU General Public License version 3. + +# Defaults. +DB_FILE=".gitattributsdb" # Database file, relative to the repository root. +DB_EXTRA=".gitattributsdb-extra" # List of base64 encoded filenames (one per line) to also store/restore attributes for. + # To add entries to this file, use: printf "%s" "" | base64 -w 0 >>.gitattributsdb-extra + # Where '' is relative to the repository root. + +# Variables. +declare -A DB_OWNERSHIPS DB_MODES DB_ACLS +# shellcheck disable=SC2155 +declare PLATFORM="$(uname -s)" + +# Function to output a log/info message. +log() { + printf "%s: %s\\n" "${0##*/}" "$*" +} + +# Function to output a warning message to stderr. +warn() { + log "Warning:" "$*" >&2 +} + +# Function to output an error message to stderr and die. +error() { + log "Error:" "$*" >&2 + exit 1 +} + +# Function to display help. +show_help() { + local SCRIPT="${0##*/}" + + #........1.........2.........3.........4.........5.........6.........7.........8 + cat <<-EOF + Usage: $SCRIPT [options] + Store and restore file attributes for files within the git repository from a + database stored within the repository itself. + + This program is intended to be called from git hooks, rather than directly. + + # FIXME: Write some info about using as a submodule and as a hook in .githooks/. + EOF +} + +# Function to read the database into an array. +read_db() { + local FILENAME MODE OWNERSHIP ACL + + # Do nothing if the DB file doesn't exist. + [[ ! -e "$DB_FILE" ]] && return 0 + + # Read the file. + while read -r FILENAME OWNERSHIP MODE ACL; do + # Store the attributes in arrays. + DB_OWNERSHIPS[$FILENAME]="$OWNERSHIP" + DB_MODES[$FILENAME]="$MODE" + DB_ACLS[$FILENAME]="$ACL" + done < <(grep -Ev '^(#|$)' "$DB_FILE") + + return 0 +} + +# Function to store file attributes into the database. +store_attributes() { + local COUNT=0 DB_TMP FILE X + + # Informational message. + log "Storing file attributes into database" + + # Use a temporary file for the new database. + DB_TMP="$(mktmp "$DB_FILE.XXXXXX" 2>/dev/null)" || error "Failed to create temporary database file" + + # While Darwin supports ACLs, there is no standard output and input format for them - don't even try. + [[ "$PLATFORM" == "Darwin" ]] && warn "Not storing ACLs on Darwin" + + # File header. + printf "# %s\\n" "This is the gitattributsdb database file." >"$DB_TMP" + printf "# %s\\n\\n" "Do not manually edit this file - any changes will be overwritten." >>"$DB_TMP" + + # Create the database. + while read -r -d $'\0' FILE; do + # No need to process the database files themselves. + [[ "$FILE" == "$DB_FILE" ]] || [[ "$FILE" == "$DB_EXTRA" ]] && continue + + if [[ "$PLATFORM" == "Linux" ]]; then + # On Linux, we can handle ACLs too. + printf "%s %s %s\\n" "$(printf "%s" "$FILE" | base64 -w 0 2>/dev/null)" "$(stat --printf '%U:%G %.4a' -- "$FILE" 2>/dev/null)" \ + "$(getfacl -cEsp -- "$FILE" 2>/dev/null | base64 -w 0 2>/dev/null)" >>"$DB_TMP" + elif [[ "$PLATFORM" == "Darwin" ]]; then + # Darwin just has to be different. + # Use the full path to Darwin's stat, in case there's a macports/brew/etc version installed. + printf "%s %s\\n" "$(printf "%s" "$FILE" | base64 2>/dev/null)" "$(/usr/bin/stat -f "%Su:%Sg %Mp%Lp" -- "$FILE" 2>/dev/null)" >>"$DB_TMP" + fi + + (( COUNT++ )) + done < <(git ls-files -z --full-name -- . 2>/dev/null; while read -r X; do printf "%s\\0" "$(printf "%s" "$X" | base64 -d 2>/dev/null)"; done < \ + <(grep -Ev '^(#|$)' "$DB_EXTRA" 2>/dev/null)) + + # Move the temporary file into place. + mv -- "$DB_TMP" "$DB_FILE" 2>/dev/null || { rm -f -- "$DB_TMP"; error "Failed to move database temporary file into place"; } + + info "$COUNT entries stored" + + # Add the databases themselves to the commit. + git add --all -f -- "$DB_EXTRA" 2>/dev/null # OK to fail silently. + git add --all -f -- "$DB_FILE" 2>/dev/null || error "Failed to add database files to commit" + + return 0 +} + +# Function to restore file attributes from the database. +restore_attributes() { + local COUNT=0 FILE ID + + # Informational message. + log "Restoring file attributes from database" + + # Read the database. + read_db + + # While Darwin supports ACLs, there is no standard output and input format for them - don't even try. + [[ "$PLATFORM" == "Darwin" ]] && warn "Not restoring ACLs on Darwin" + + # Restore from the read database. + while read -r ID; do + # Decode the filename from the array ID. + FILE="$(printf "%s" "$ID" | base64 -d 2>/dev/null)" || { warn "Failed to decode filename: $ID"; continue; } + + # Ignore empty filenames, or non-existant files. + [[ -z "$FILE" ]] || [[ ! -e "$FILE" ]] && continue + + # Don't restore attributes for symlinks. + [[ -L "$FILE" ]] && warn "Not restoring attributes for symlink: $FILE" && continue + + # Restore ownerships. + chown -- "${DB_OWNERSHIPS[$ID]}" "$FILE" 2>/dev/null || warn "Failed to restore ownership: $FILE" + + # Store mode. + chmod -- "${DB_MODES[$ID]}" "$FILE" 2>/dev/null || warn "Failed to restore permissions: $FILE" + + # Restore ACLs on Linux. + [[ "$PLATFORM" == "Linux" ]] && { + printf "%s" "${DB_ACLS[$ID]}" | base64 -d 2>/dev/null | setfacl -M - "$FILE" 2>/dev/null || warn "Failed to restore ACL: $FILE" + } + + (( COUNT++ )) + done < <(printf "%s\\n" "${!DB_OWNERSHIPS[@]}") + + info "$COUNT entries restored" + + return 0 +} + +# Exit if bash isn't v4+. +(( BASH_VERSINFO[0] < 4 )) && error "Bash v4 or later is required" + +# Change to the root directory of the repository. +REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" +[[ -z "$REPO_ROOT" ]] && error "Could not determine git repository root" +pushd -- "$REPO_ROOT" >/dev/null 2>&1 || error "Failed to switch to git repository root" + +# Parse command line. +(( $# == 0 )) && { + printf "%s: %s\\n" "${0##*/}" "missing argument" >&2 + printf "%s: %s %s\\n" "Try" "${0##*/}" "--help" >&2 + exit 1 +} +case "$1" in + '-h'|'--h'|'--help') + show_help + ;; + 'post-checkout'|'post-merge') + # Restore the file attributes from the database. + restore_attributes + ;; + 'pre-commit') + # Store the file attributes into the database. + store_attributes + ;; + *) + printf "%s: %s: %s\\n" "${0##*/}" "invalid option" "$1" >&2 + printf "%s: %s %s\\n" "Try" "${0##*/}" "--help" >&2 + exit 1 + ;; +esac + +exit 0