Logic changes within the hook. Expand glob entries in the extra list.

This commit is contained in:
Darren 'Tadgy' Austin 2024-08-25 19:38:35 +01:00
commit 522b711313
2 changed files with 105 additions and 64 deletions

View file

@ -151,4 +151,6 @@ To add paths to the "extra" files database, use:
``` ```
Where `<path>` is a path relative to the repository root. Where `<path>` is a path relative to the repository root.
The `<path>` is expanded while being processed, so may contain bash pathname glob characters.
Old paths (that no longer exist on the filesystem) stored in the `.gitattributesdb-extra` file are ignored when commiting. Old paths (that no longer exist on the filesystem) stored in the `.gitattributesdb-extra` file are ignored when commiting.

View file

@ -1,14 +1,14 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Version: 0.0.2 # Version: 0.1.0
# Copyright (c) 2023: # Copyright (c) 2023-2024:
# Darren 'Tadgy' Austin <darren (at) afterdark.org.uk> # Darren 'Tadgy' Austin <darren (at) afterdark.org.uk>
# Licensed under the terms of the GNU General Public License version 3. # Licensed under the terms of the GNU General Public License version 3.
# Defaults. # Defaults.
DB_FILE=".gitattributesdb" # Database file, relative to the repository root. DB_FILE=".gitattributesdb" # Database file, relative to the repository root.
DB_EXTRA=".gitattributesdb-extra" # List of base64 encoded filenames (one per line) to also store/restore attributes for. DB_EXTRA=".gitattributesdb-extra" # List of base64 encoded paths (one per line) to also store/restore attributes for.
# To add entries to this file, use: printf "%s" "<filename>" | base64 -w 0 >>.gitattributesdb-extra # To add entries to this file, use: printf "%s" "<path>" | base64 -w 0 >>.gitattributesdb-extra
# Where '<filename>' is relative to the repository root. # Where '<path>' is relative to the repository root.
# Variables. # Variables.
declare -A DB_ACLS DB_ATIMES DB_MODES DB_MTIMES DB_OWNERSHIPS DB_XATTRS declare -A DB_ACLS DB_ATIMES DB_MODES DB_MTIMES DB_OWNERSHIPS DB_XATTRS
@ -38,8 +38,8 @@ show_help() {
#........1.........2.........3.........4.........5.........6.........7.........8 #........1.........2.........3.........4.........5.........6.........7.........8
cat <<-EOF cat <<-EOF
Usage: $SCRIPT [options] <hook name> Usage: $SCRIPT [options] <hook name>
Store and restore file attributes for files within the git repository from a Store and restore attributes for paths within the git repository from a database
database stored within the repository itself. stored within the repository itself.
Options: Options:
-h|--help Display this help page. -h|--help Display this help page.
@ -49,36 +49,64 @@ show_help() {
full installation/usage instructions. full installation/usage instructions.
This program is intended to be called from git hooks, rather than directly. This program is intended to be called from git hooks, rather than directly.
Copyright (c) 2023-2024:
Darren 'Tadgy' Austin <darren (at) afterdark.org.uk>
Licensed under the terms of the GNU General Public License version 3.
EOF EOF
} }
# Function to read the database into an array. # Function to read the database into an array.
read_db() { read_db_entries() {
local ACL ATIME FILENAME MODE MTIME OWNERSHIP XATTR local ACL ATIME MODE MTIME OWNERSHIP PATHNAME XATTR
# Do nothing if the DB file doesn't exist. # Do nothing if the DB file doesn't exist, isn't a regular file or is empty.
[[ ! -e "$DB_FILE" ]] && return 0 [[ ! -f "$DB_FILE" ]] || [[ ! -s "$DB_FILE" ]] && return 0
# Read the file. # Read the file.
while read -r FILENAME MTIME ATIME OWNERSHIP MODE ACL XATTR; do while read -r PATHNAME MTIME ATIME OWNERSHIP MODE ACL XATTR; do
# Store the attributes in arrays. # Store the attributes in arrays.
DB_MTIMES[$FILENAME]="$MTIME" DB_MTIMES[$PATHNAME]="$MTIME"
DB_ATIMES[$FILENAME]="$ATIME" DB_ATIMES[$PATHNAME]="$ATIME"
DB_OWNERSHIPS[$FILENAME]="$OWNERSHIP" DB_OWNERSHIPS[$PATHNAME]="$OWNERSHIP"
DB_MODES[$FILENAME]="$MODE" DB_MODES[$PATHNAME]="$MODE"
DB_ACLS[$FILENAME]="$ACL" DB_ACLS[$PATHNAME]="$ACL"
DB_XATTRS[$FILENAME]="$XATTR" DB_XATTRS[$PATHNAME]="$XATTR"
done < <(grep -Ev '^(#|$)' "$DB_FILE") done < <(grep -Ev '^(#|$)' "$DB_FILE")
return 0 return 0
} }
# Function to store file attributes into the database. # Function to store path attributes into the database.
add_db_entry() {
local ERR=0
[[ -z "$1" ]] && return 1
if [[ "$PLATFORM" == "Linux" ]]; then
# On Linux, we can handle ACLs and xattrs too.
ACL="$(getfacl -cEsp -- "$1" 2>/dev/null | base64 -w 0 2>/dev/null)"
XATTR="$(getfattr -dhe base64 -- "$1" 2>/dev/null | base64 -w 0 2>/dev/null)"
printf "%s %s %s %s\\n" "$(printf "%s" "$1" | base64 -w 0 2>/dev/null)" "$(stat --printf '%.9Y %.9X %U:%G %.4a' -- "$1" 2>/dev/null)" "${ACL:--}" \
"${XATTR:--}" >>"$DB_TMP" || { warn "Failed to add database entry: $1"; ERR=1; }
elif [[ "$PLATFORM" == "Darwin" ]]; then
# Darwin just has to be different, so no ACLs or xattrs.
# Use the full path to Darwin's stat, in case there's a macports/brew/etc version installed.
printf "%s %s\\n" "$(printf "%s" "$1" | base64 -b 0 2>/dev/null)" "$(/usr/bin/stat -f '%Fm %Fa %Su:%Sg %Mp%Lp' -- "$1" 2>/dev/null)" >>"$DB_TMP" || \
{ warn "Failed to add database entry: $1"; ERR=1; }
else
error "Unsupported platform: $PLATFORM"
fi
return "$ERR"
}
# Process the paths to add to the database.
store_attributes() { store_attributes() {
local ACL COUNT=0 DB_TMP EXTRA FILE XATTR local ACL ADD_COUNT=0 DB_TMP ERR_COUNT=0 EXTRA NAME PATHNAME XATTR
# Informational message. # Informational message.
log "Storing file attributes into database" log "Storing path attributes into database"
# Use a temporary file for the new database. # Use a temporary file for the new database.
DB_TMP="$(mktemp "$DB_FILE.XXXXXX" 2>/dev/null)" || error "Failed to create temporary database file" DB_TMP="$(mktemp "$DB_FILE.XXXXXX" 2>/dev/null)" || error "Failed to create temporary database file"
@ -91,30 +119,40 @@ store_attributes() {
printf "# %s\\n\\n" "Do not manually edit this file - any changes will be overwritten." >>"$DB_TMP" printf "# %s\\n\\n" "Do not manually edit this file - any changes will be overwritten." >>"$DB_TMP"
# Create the database. # Create the database.
while read -r -d $'\0' FILE; do while read -r -d $'\0' PATHNAME; do
# No need to process the database files themselves. # No need to process the database files themselves.
[[ "$FILE" == "$DB_FILE" ]] || [[ "$FILE" == "$DB_EXTRA" ]] && continue [[ "$PATHNAME" == "$DB_FILE" ]] || [[ "$PATHNAME" == "$DB_EXTRA" ]] && continue
if [[ "$PLATFORM" == "Linux" ]]; then # Add the path's attributes to the database.
# On Linux, we can handle ACLs and xattrs too. if add_db_entry "$PATHNAME"; then
ACL="$(getfacl -cEsp -- "$FILE" 2>/dev/null | base64 -w 0 2>/dev/null)" (( ADD_COUNT++ ))
XATTR="$(getfattr -dhe base64 -- "$FILE" 2>/dev/null | base64 -w 0 2>/dev/null)" else
printf "%s %s %s %s\\n" "$(printf "%s" "$FILE" | base64 -w 0 2>/dev/null)" "$(stat --printf '%.9Y %.9X %U:%G %.4a' -- "$FILE" 2>/dev/null)" \ (( ERR_COUNT++ ))
"${ACL:--}" "${XATTR:--}" >>"$DB_TMP"
elif [[ "$PLATFORM" == "Darwin" ]]; then
# Darwin just has to be different, so no ACLs or xattrs.
# 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 -b 0 2>/dev/null)" "$(/usr/bin/stat -f '%Fm %Fa %Su:%Sg %Mp%Lp' -- "$FILE" 2>/dev/null)" >>"$DB_TMP"
fi fi
done < <(git ls-files -z --full-name -- . 2>/dev/null)
while read -r -d $'\0' PATHNAME; do
# PATHNAME should not be quoted - it needs to be expanded.
for NAME in $PATHNAME; do
# If the path doesn't exist, ignore it.
[[ ! -e "$NAME" ]] && continue
(( COUNT++ )) # No need to process the database files themselves.
done < <(git ls-files -z --full-name -- . 2>/dev/null; while read -r EXTRA; do printf "%s\\0" "$(printf "%s" "$EXTRA" | base64 -d 2>/dev/null)"; done < \ [[ "$NAME" == "$DB_FILE" ]] || [[ "$NAME" == "$DB_EXTRA" ]] && continue
<(grep -Ev '^(#|$)' "$DB_EXTRA" 2>/dev/null))
# Add the path's attributes to the database.
if add_db_entry "$NAME"; then
(( ADD_COUNT++ ))
else
(( ERR_COUNT++ ))
fi
done
done < <(while read -r EXTRA; do printf "%s%b" "$(printf "%s" "$EXTRA" | base64 -d 2>/dev/null)" "\\0"; done < <(grep -Ev '^(#|$)' "$DB_EXTRA" 2>/dev/null))
# Move the temporary file into place. # 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"; } mv -- "$DB_TMP" "$DB_FILE" 2>/dev/null || { rm -f -- "$DB_TMP"; error "Failed to move database temporary file into place"; }
log "$COUNT entries stored" (( ADD_COUNT > 0 )) && log "$ADD_COUNT entries stored"
(( ERR_COUNT > 0 )) && warn "$ERR_COUNT failied entries"
# Add the databases themselves to the commit. # 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_EXTRA" 2>/dev/null # OK to fail silently.
@ -123,57 +161,58 @@ store_attributes() {
return 0 return 0
} }
# Function to restore file attributes from the database. # Function to restore path attributes from the database.
restore_attributes() { restore_attributes() {
local COUNT=0 FILE ID local COUNT=0 ID PATHNAME WARN
# Informational message. # Informational message.
log "Restoring file attributes from database" log "Restoring path attributes from database"
# Read the database. # Read the database.
read_db read_db_entries
# While Darwin supports ACLs, there is no standard output and input format for them - don't even try. # While Darwin supports ACLs, there is no standard output and input format for them - don't even try.
[[ "$PLATFORM" == "Darwin" ]] && warn "Not restoring ACLs or xattrs on Darwin" [[ "$PLATFORM" == "Darwin" ]] && warn "Not restoring ACLs or xattrs on Darwin"
# Restore from the read database. # Restore from the read database.
while read -r ID; do while read -r ID; do
# Decode the filename from the array ID. # Decode the path name from the array ID.
FILE="$(printf "%s" "$ID" | base64 -d 2>/dev/null)" || { warn "Failed to decode filename: $ID"; continue; } PATHNAME="$(printf "%s" "$ID" | base64 -d 2>/dev/null)" || { warn "Failed to decode path: $ID"; continue; }
# Ignore empty filenames, or non-existant files. # Ignore empty path names, or non-existant paths.
[[ -z "$FILE" ]] || [[ ! -e "$FILE" ]] && continue [[ -z "$PATHNAME" ]] || [[ ! -e "$PATHNAME" ]] && continue
# Don't restore attributes for symlinks. # Don't restore attributes for symlinks.
[[ -L "$FILE" ]] && warn "Not restoring attributes for symlink: $FILE" && continue [[ -L "$PATHNAME" ]] && warn "Not restoring attributes for symlink: $PATHNAME" && continue
# Restore ownerships. # Restore ownerships.
chown -- "${DB_OWNERSHIPS[$ID]}" "$FILE" 2>/dev/null || warn "Failed to restore ownership: $FILE" chown -- "${DB_OWNERSHIPS[$ID]}" "$PATHNAME" 2>/dev/null || { warn "Failed to restore ownership: $PATHNAME"; WARN=1; }
# Store mode. # Restore mode.
chmod -- "${DB_MODES[$ID]}" "$FILE" 2>/dev/null || warn "Failed to restore permissions: $FILE" chmod -- "${DB_MODES[$ID]}" "$PATHNAME" 2>/dev/null || { warn "Failed to restore permissions: $PATHNAME"; WARN=1; }
# Restore ACLs on Linux. # Restore {a,m}times (and ACLs on Linux).
if [[ "$PLATFORM" == "Linux" ]]; then if [[ "$PLATFORM" == "Linux" ]]; then
touch -m --date="$(date --date="19700101 00:00:00 + ${DB_MTIMES[$ID]} seconds" +'%Y/%m/%d %H:%M:%S.%N' 2>/dev/null)" -- "$FILE" 2>/dev/null || \ touch -m --date="$(date --date="19700101 00:00:00 + ${DB_MTIMES[$ID]} seconds" +'%Y/%m/%d %H:%M:%S.%N' 2>/dev/null)" -- "$PATHNAME" 2>/dev/null || \
warn "Failed to restore mtime: $FILE" { warn "Failed to restore mtime: $PATHNAME"; WARN=1; }
touch -a --date="$(date --date="19700101 00:00:00 + ${DB_ATIMES[$ID]} seconds" +'%Y/%m/%d %H:%M:%S.%N' 2>/dev/null)" -- "$FILE" 2>/dev/null || \ touch -a --date="$(date --date="19700101 00:00:00 + ${DB_ATIMES[$ID]} seconds" +'%Y/%m/%d %H:%M:%S.%N' 2>/dev/null)" -- "$PATHNAME" 2>/dev/null || \
warn "Failed to restore atime: $FILE" { warn "Failed to restore atime: $PATHNAME"; WARN=1; }
[[ "${DB_ACLS[$ID]}" != "-" ]] && { printf "%s" "${DB_ACLS[$ID]}" | base64 -d 2>/dev/null | setfacl -M - -- "$FILE" 2>/dev/null || \ [[ "${DB_ACLS[$ID]}" != "-" ]] && { printf "%s" "${DB_ACLS[$ID]}" | base64 -d 2>/dev/null | setfacl -M - -- "$PATHNAME" 2>/dev/null || \
warn "Failed to restore ACLs: $FILE"; } warn "Failed to restore ACLs: $PATHNAME"; WARN=1; }
[[ "${DB_XATTRS[$ID]}" != "-" ]] && { printf "%s" "${DB_XATTRS[$ID]}" | base64 -d 2>/dev/null | setfattr --restore=- 2>/dev/null || \ [[ "${DB_XATTRS[$ID]}" != "-" ]] && { printf "%s" "${DB_XATTRS[$ID]}" | base64 -d 2>/dev/null | setfattr --restore=- 2>/dev/null || \
warn "Failed to restore xattrs: $FILE"; } warn "Failed to restore xattrs: $PATHNAME"; WARN=1; }
elif [[ "$PLATFORM" == "Darwin" ]]; then elif [[ "$PLATFORM" == "Darwin" ]]; then
touch -m -d "$(date -j -r "${DB_MTIMES[$ID]%.*}" +"%Y-%m-%dT%H:%M:%S.${DB_MTIMES[$ID]#*.}")" -- "$FILE" 2>/dev/null || \ touch -m -d "$(date -j -r "${DB_MTIMES[$ID]%.*}" +"%Y-%m-%dT%H:%M:%S.${DB_MTIMES[$ID]#*.}")" -- "$PATHNAME" 2>/dev/null || \
warn "Failed to restore mtime: $FILE" { warn "Failed to restore mtime: $PATHNAME"; WARN=1; }
touch -a -d "$(date -j -r "${DB_ATIMES[$ID]%.*}" +"%Y-%m-%dT%H:%M:%S.${DB_ATIMES[$ID]#*.}")" -- "$FILE" 2>/dev/null || \ touch -a -d "$(date -j -r "${DB_ATIMES[$ID]%.*}" +"%Y-%m-%dT%H:%M:%S.${DB_ATIMES[$ID]#*.}")" -- "$PATHNAME" 2>/dev/null || \
warn "Failed to restore atime: $FILE" { warn "Failed to restore atime: $PATHNAME"; WARN=1; }
fi fi
(( COUNT++ )) (( COUNT++ ))
done < <(printf "%s\\n" "${!DB_OWNERSHIPS[@]}") done < <(printf "%s\\n" "${!DB_OWNERSHIPS[@]}")
log "$COUNT entries restored" # shellcheck disable=SC2015
[[ ! -v WARN ]] && log "$COUNT entries restored" || log "$COUNT entries restored (with warnings)"
return 0 return 0
} }
@ -197,11 +236,11 @@ case "$1" in
show_help show_help
;; ;;
'post-checkout'|'post-merge') 'post-checkout'|'post-merge')
# Restore the file attributes from the database. # Restore the path attributes from the database.
restore_attributes restore_attributes
;; ;;
'pre-commit') 'pre-commit')
# Store the file attributes into the database. # Store the path attributes into the database.
store_attributes store_attributes
;; ;;
*) *)