Skip to content

Instantly share code, notes, and snippets.

@kou1okada
Last active January 11, 2022 11:52
Show Gist options
  • Save kou1okada/3e591ceb2e8358eb11aa21887f0e07f2 to your computer and use it in GitHub Desktop.
Save kou1okada/3e591ceb2e8358eb11aa21887f0e07f2 to your computer and use it in GitHub Desktop.
git-preserving-mtimt: Git client hooks for preserving mtime.

git-preserving-mtimt: Git client hooks for preserving mtime.

git preserving-mtime saves timestamps for each file into the commit message as below format:


```timestamp
2018-11-19 13:05:04.193545100 +0900	'README.md'
...
```

Requirements

Installation.

Install

cd $repos
git clone https://gist.github.com/kou1okada/3e591ceb2e8358eb11aa21887f0e07f2 git-preserving-mtime
cd git-preserving-mtime
ln -s `pwd`/git-preserving-mtime /usr/local/bin/

Uninstall

rm /usr/local/bin/git-preserving-mtime

Usage

git-preserving-mtime install   [<repos>] # Install client hooks to repository.
git-preserving-mtime uninstall [<repos>] # Uninstall client hooks to repository.
git-preserving-mtime enable    [<repos>] # Enable client hooks to repository.
git-preserving-mtime disable   [<repos>] # Disable client hooks to repository.

Note

You must install hooks to each repositories which you cloned, because client-side hooks are not copied when a repository is cloned.

More details, see Pro Git book 8.3 Git Hooks.

Copyright

Copyright (c) 2018 Koichi OKADA. All rights reserved.

License

The MIT license.

#!/usr/bin/env bash
source hhs.bash 0.2.0
function strmaxlen () # [<strings> ...]
# Get max length of strings.
{
local s len=0
for s; do (( len < ${#s} )) && let len=${#s}; done
echo $len
}
function escape_sed_regex () # <string>
# Escape special characters of sed regex in a string.
{
sed -re 's/[][$*+.()\/\\{|}]/\\\0/g' <<< "$1"
}
function git_preserving_mtime_post_checkout () # [<options>]
# Git hooks for post-checkout.
{
local log lines stamps stamp mtime fn
log=( git log -1 --pretty="format:%B" )
lines=( $("${log[@]}" | grep -nE '```' | grep -EA1 '```timestamp' | sed -re 's/:.*//g') )
if ! [[ "${lines[0]}" =~ ^[0-9]+$ && "${lines[1]}" =~ ^[0-9]+$ ]]; then
warning "No timestamp found."
exit
fi
readarray -t stamps < <("${log[@]}" | headtail $(( lines[0] + 1 )) $(( lines[1] - 1)) )
for stamp in "${stamps[@]}"; do
mtime="${stamp%%$'\t'*}"
fn="$(echo "x${stamp#*$'\t'}x" | xargs echo)"
fn="${fn:1:-1}"
echo "$stamp"
[ -n "$OPT_DRY_RUN" ] && continue
touch -d "$mtime" "$fn"
done
}
function git_preserving_mtime_prepare_commit_msg () # [<options>] <COMMIT_MSG_FILE> <COMMIT_SOURCE> <SHA1>
# Git hooks for prepare-commit-msg.
{
local files file tmp
readarray -t files < <(git ls-files)
tmp="$(mktemp -p "$(dirname "$1")" "$(basename "$1"),XXXXXXXX")"
cp -a "$1" "$tmp"
{
echo
echo '```timestamp'
for file in "${files[@]}"; do
stat -c "%y"$'\t'"%N" "$file"
done
echo '```'
cat "$tmp"
} >"$1"
rm "$tmp"
}
function common_init_git_preserving_mtime () # [<options>] [<repos> [<targets> ...]]
# Initialization for git-preserving-mtime
# Parameters:
# <repos> : Directory of git repository
# <targets> : Hook targets for git hooks (ex: .git/hooks/post-checkout).
# If this parameter is given, <repos> will be ignored.
# Returns:
# invoker : Invoke string for git-preserving-mtime.
# invoker_regex : Invoker string which escape special characters of regex.
# repos : Directory of repository (default: ./).
# hookdir : Directory of githooks (default: .git/hooks).
# targets : Target files of githooks (default: .git/hooks/{prepare-commit-msg,post-checkout}).
{
invoker='git preserving-mtime "${0##*/}" "$@"'
invoker_regex="$(escape_sed_regex "$invoker")"
repos=${1:-.}
hookdir="${repos%/}/.git/hooks"
(( $# < 2 )) && targets=( "${hookdir%/}/"{prepare-commit-msg,post-checkout} ) || targets=( "${@:2}" )
w="$(strmaxlen "${targets[@]}")"
}
function git_preserving_mtime_status () # [<options>] [<repos> [<targets> ...]]
# Show status of preserving-mtime.
# Parameters:
# <repos> : Directory of git repository
# <targets> : Hook targets for git hooks (ex: .git/hooks/post-checkout).
# If this parameter is given, <repos> will be ignored.
{
local invoker invoker_regex repos hookdir targets w file status
common_init_git_preserving_mtime "$@"
for file in "${targets[@]}"; do
if [ ! -e "$file" ]; then
status="${SGR_rfg_red}${SGR_bold}File is not found${SGR_reset}"
elif head -n1 "$file" | grep -q bash; then
if status="$(grep -E "^#?${invoker_regex}$" "$file")"; then
[[ "${status:0:1}" != "#" ]] && status="${SGR_fg_green}${SGR_bold}enabled${SGR_reset}" || status="${SGR_fg_blue}${SGR_bold}disabled${SGR_reset}"
else
status="${SGR_fg_yellow}${SGR_bold}Never installed${SGR_reset}"
fi
else
status="${SGR_red}${SGR_bold}Not bash script.${SGR_reset}"
fi
printf "%-${w}s : %b\n" "$file" "$status" >&2
done
}
function git_preserving_mtime_install () # [<options>] [<repos> [<targets> ...]]
# Install git hooks for preserving-mtime.
# Parameters:
# <repos> : Directory of git repository
# <targets> : Hook targets for git hooks (ex: .git/hooks/post-checkout).
# If this parameter is given, <repos> will be ignored.
{
local invoker invoker_regex repos hookdir targets w file status
common_init_git_preserving_mtime "$@"
for file in "${targets[@]}"; do
if [ ! -e "$file" ]; then
{
echo "#!/usr/bin/env bash"
echo "$invoker"
} >"$file"
chmod +x "$file"
status="Created file and installed."
elif head -n1 "$file" | grep -q bash; then
if status="$(grep -E "^#?${invoker_regex}$" "$file")"; then
if [[ "${status:0:1}" != "#" ]]; then
sed -r -i -e 's/^#?('"$invoker_regex"')$/\1/g' "$file"
status="Enabled."
else
status="Already enabled."
fi
else
echo "$invoker" >> "$file"
status="Installed."
fi
else
status="${SGR_red}${SGR_bold}Not bash script.${SGR_reset}"
fi
printf "%-${w}s : %b\n" "$file" "$status" >&2
done
}
function git_preserving_mtime_enable () # [<options>] [<repos> [<targets> ...]]
# Enable git hooks for preserving-mtime.
# Parameters:
# <repos> : Directory of git repository
# <targets> : Hook targets for git hooks (ex: .git/hooks/post-checkout).
# If this parameter is given, <repos> will be ignored.
{
local invoker invoker_regex repos hookdir targets w file status
common_init_git_preserving_mtime "$@"
for file in "${targets[@]}"; do
if [ ! -e "$file" ]; then
{
echo "#!/usr/bin/env bash"
echo "$invoker"
} >"$file"
chmod +x "$file"
status="Created file and installed."
elif head -n1 "$file" | grep -q bash; then
if status="$(grep -E "^#?${invoker_regex}$" "$file")"; then
if [[ "${status:0:1}" == "#" ]]; then
sed -r -i -e 's/^#?('"$invoker_regex"')$/\1/g' "$file"
status="Enabled."
else
status="Already enabled."
fi
else
echo "$invoker" >> "$file"
status="Installed."
fi
else
status="${SGR_red}${SGR_bold}Not bash script.${SGR_reset}"
fi
printf "%-${w}s : %b\n" "$file" "$status" >&2
done
}
function git_preserving_mtime_disable () # [<options>] [<repos> [<targets> ...]]
# Disable git hooks for preserving-mtime.
# Parameters:
# <repos> : Directory of git repository
# <targets> : Hook targets for git hooks (ex: .git/hooks/post-checkout).
# If this parameter is given, <repos> will be ignored.
{
local invoker invoker_regex repos hookdir targets w file status
common_init_git_preserving_mtime "$@"
for file in "${targets[@]}"; do
if [ ! -e "$file" ]; then
status="File is not found."
elif head -n1 "$file" | grep -q bash; then
if status="$(grep -E "^#?${invoker_regex}$" "$file")"; then
if [[ "${status:0:1}" != "#" ]]; then
sed -r -i -e 's/^#?('"$invoker_regex"')$/#\1/g' "$file"
status="Disabled."
else
status="Already disabled."
fi
else
status="Never installed."
fi
else
status="${SGR_red}${SGR_bold}Not bash script.${SGR_reset}"
fi
printf "%-${w}s : %b\n" "$file" "$status" >&2
done
}
function git_preserving_mtime_uninstall () # [<options>] [<repos> [<targets> ...]]
# Uninstall git hooks for preserving-mtime.
# Parameters:
# <repos> : Directory of git repository
# <targets> : Hook targets for git hooks (ex: .git/hooks/post-checkout).
# If this parameter is given, <repos> will be ignored.
{
local invoker invoker_regex repos hookdir targets w file status
common_init_git_preserving_mtime "$@"
for file in "${targets[@]}"; do
if [ ! -e "$file" ]; then
status="File is not found."
elif head -n1 "$file" | grep -q bash; then
if status="$(grep -E "^#?${invoker_regex}$" "$file")"; then
sed -r -i -e '/^#?'"$invoker_regex"'$/d' "$file"
status="Uninstalled."
else
status="Never installed."
fi
else
status="${SGR_red}${SGR_bold}Not bash script.${SGR_reset}"
fi
printf "%-${w}s : %b\n" "$file" "$status" >&2
done
}
has_subcommand_git_preserving_mtime=1
function optparse_git_preserving_mtime ()
{
case "$1" in
-n|--dry-run)
# Do not change anything.
nparams 0
optset DRY_RUN "$1"
;;
*) return 1 ;;
esac
}
function usage_git_preserving_mtime ()
{
echo "Usage: git preserving-mtime [COMMAND]"
echo " Preserving mtime."
usage__commands
usage__options
}
function git_preserving_mtime () # [<options>] [<command>]
{
invoke_usage
}
invoke_command "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment