517 lines
14 KiB
Bash
Executable file
517 lines
14 KiB
Bash
Executable file
#!/bin/bash
|
||
# Source: https://gitlab.gnome.org/GNOME/fractal/blob/master/hooks/pre-commit.hook
|
||
|
||
export LC_ALL=C
|
||
|
||
# Usage info
|
||
show_help() {
|
||
cat << EOF
|
||
Run conformity checks on the current Rust project.
|
||
|
||
If a dependency is not found, helps the user to install it.
|
||
|
||
USAGE: ${0##*/} [OPTIONS]
|
||
|
||
OPTIONS:
|
||
-s, --git-staged Only check files staged to be committed
|
||
-f, --force-install Install missing dependencies without asking
|
||
-v, --verbose Use verbose output
|
||
-h, --help Display this help and exit
|
||
|
||
ERROR CODES:
|
||
1 Check failed
|
||
2 Missing dependency
|
||
EOF
|
||
}
|
||
|
||
# Style helpers
|
||
act="\e[1;32m"
|
||
err="\e[1;31m"
|
||
pos="\e[32m"
|
||
neg="\e[31m"
|
||
res="\e[0m"
|
||
|
||
# Common styled strings
|
||
Installing="${act}Installing${res}"
|
||
Checking=" ${act}Checking${res}"
|
||
Failed=" ${err}Failed${res}"
|
||
error="${err}error:${res}"
|
||
invalid="${neg}Invalid input${res}"
|
||
ok="${pos}ok${res}"
|
||
fail="${neg}fail${res}"
|
||
|
||
# Initialize variables
|
||
git_staged=0
|
||
force_install=0
|
||
verbose=0
|
||
|
||
# Helper functions
|
||
# Sort to_sort in natural order.
|
||
sort() {
|
||
local size=${#to_sort[@]}
|
||
local swapped=0;
|
||
|
||
for (( i = 0; i < $size-1; i++ ))
|
||
do
|
||
swapped=0
|
||
for ((j = 0; j < $size-1-$i; j++ ))
|
||
do
|
||
if [[ "${to_sort[$j]}" > "${to_sort[$j+1]}" ]]
|
||
then
|
||
temp="${to_sort[$j]}";
|
||
to_sort[$j]="${to_sort[$j+1]}";
|
||
to_sort[$j+1]="$temp";
|
||
swapped=1;
|
||
fi
|
||
done
|
||
|
||
if [[ $swapped -eq 0 ]]; then
|
||
break;
|
||
fi
|
||
done
|
||
}
|
||
|
||
# Remove common entries in to_diff1 and to_diff2.
|
||
diff() {
|
||
for i in ${!to_diff1[@]}; do
|
||
for j in ${!to_diff2[@]}; do
|
||
if [[ "${to_diff1[$i]}" == "${to_diff2[$j]}" ]]; then
|
||
unset to_diff1[$i]
|
||
unset to_diff2[$j]
|
||
break
|
||
fi
|
||
done
|
||
done
|
||
}
|
||
|
||
# Check if rustup is available.
|
||
# Argument:
|
||
# '-i' to install if missing.
|
||
check_rustup() {
|
||
if ! which rustup &> /dev/null; then
|
||
if [[ "$1" == '-i' ]]; then
|
||
echo -e "$Installing rustup…"
|
||
curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain nightly
|
||
export PATH=$PATH:$HOME/.cargo/bin
|
||
if ! which rustup &> /dev/null; then
|
||
echo -e "$Failed to install rustup"
|
||
exit 2
|
||
fi
|
||
else
|
||
exit 2
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# Install cargo via rustup.
|
||
install_cargo() {
|
||
check_rustup -i
|
||
if ! which cargo >/dev/null 2>&1; then
|
||
echo -e "$Failed to install cargo"
|
||
exit 2
|
||
fi
|
||
}
|
||
|
||
# Check if cargo is available. If not, ask to install it.
|
||
check_cargo() {
|
||
if ! which cargo >/dev/null 2>&1; then
|
||
echo "Unable to find cargo for pre-commit checks"
|
||
|
||
if [[ $force_install -eq 1 ]]; then
|
||
install_cargo
|
||
elif [ ! -t 1 ]; then
|
||
exit 2
|
||
elif check_rustup; then
|
||
echo -e "$error rustup is installed but the cargo command isn't available"
|
||
exit 2
|
||
else
|
||
echo ""
|
||
echo "y: Install cargo via rustup"
|
||
echo "N: Don't install cargo and abort checks"
|
||
echo ""
|
||
while true; do
|
||
echo -n "Install cargo? [y/N]: "; read yn < /dev/tty
|
||
case $yn in
|
||
[Yy]* )
|
||
install_cargo
|
||
break
|
||
;;
|
||
[Nn]* | "" )
|
||
exit 2
|
||
;;
|
||
* )
|
||
echo $invalid
|
||
;;
|
||
esac
|
||
done
|
||
fi
|
||
fi
|
||
|
||
if [[ $verbose -eq 1 ]]; then
|
||
echo ""
|
||
rustc -Vv && cargo +nightly -Vv
|
||
fi
|
||
}
|
||
|
||
# Install rustfmt with rustup.
|
||
install_rustfmt() {
|
||
check_rustup -i
|
||
|
||
echo -e "$Installing rustfmt…"
|
||
rustup component add --toolchain nightly rustfmt
|
||
if ! cargo +nightly fmt --version >/dev/null 2>&1; then
|
||
echo -e "$Failed to install rustfmt"
|
||
exit 2
|
||
fi
|
||
}
|
||
|
||
# Run rustfmt to enforce code style.
|
||
run_rustfmt() {
|
||
if ! cargo +nightly fmt --version >/dev/null 2>&1; then
|
||
if [[ $force_install -eq 1 ]]; then
|
||
install_rustfmt
|
||
elif [ ! -t 1 ]; then
|
||
echo "Unable to check Fractal’s code style, because rustfmt could not be run"
|
||
exit 2
|
||
else
|
||
echo "Rustfmt is needed to check Fractal’s code style, but it isn’t available"
|
||
echo ""
|
||
echo "y: Install rustfmt via rustup"
|
||
echo "N: Don't install rustfmt and abort checks"
|
||
echo ""
|
||
while true; do
|
||
echo -n "Install rustfmt? [y/N]: "; read yn < /dev/tty
|
||
case $yn in
|
||
[Yy]* )
|
||
install_rustfmt
|
||
break
|
||
;;
|
||
[Nn]* | "" )
|
||
exit 2
|
||
;;
|
||
* )
|
||
echo $invalid
|
||
;;
|
||
esac
|
||
done
|
||
fi
|
||
fi
|
||
|
||
echo -e "$Checking code style…"
|
||
|
||
if [[ $verbose -eq 1 ]]; then
|
||
echo ""
|
||
cargo +nightly fmt --version
|
||
echo ""
|
||
fi
|
||
|
||
|
||
if [[ $git_staged -eq 1 ]]; then
|
||
staged_files=`git diff --name-only --cached | grep '.rs$'`
|
||
result=0
|
||
for file in ${staged_files[@]}; do
|
||
|
||
if ! rustfmt --unstable-features --skip-children --check $file; then
|
||
result=1
|
||
fi
|
||
done
|
||
|
||
if [[ $result -eq 1 ]]; then
|
||
echo -e " Checking code style result: $fail"
|
||
echo "Please fix the above issues, either manually or by running: cargo fmt --all"
|
||
exit 1
|
||
else
|
||
echo -e " Checking code style result: $ok"
|
||
fi
|
||
else
|
||
if ! cargo +nightly fmt --all -- --check; then
|
||
echo -e " Checking code style result: $fail"
|
||
echo "Please fix the above issues, either manually or by running: cargo fmt --all"
|
||
exit 1
|
||
else
|
||
echo -e " Checking code style result: $ok"
|
||
fi
|
||
fi
|
||
}
|
||
|
||
|
||
# Install typos with cargo.
|
||
install_typos() {
|
||
echo -e "$Installing typos…"
|
||
cargo install typos-cli
|
||
if ! typos --version >/dev/null 2>&1; then
|
||
echo -e "$Failed to install typos"
|
||
exit 2
|
||
fi
|
||
}
|
||
|
||
# Run typos to check for spelling mistakes.
|
||
run_typos() {
|
||
if ! typos --version >/dev/null 2>&1; then
|
||
if [[ $force_install -eq 1 ]]; then
|
||
install_typos
|
||
elif [ ! -t 1 ]; then
|
||
echo "Unable to check spelling mistakes, because typos could not be run"
|
||
exit 2
|
||
else
|
||
echo "Typos is needed to check spelling mistakes, but it isn’t available"
|
||
echo ""
|
||
echo "y: Install typos via cargo"
|
||
echo "N: Don't install typos and abort checks"
|
||
echo ""
|
||
while true; do
|
||
echo -n "Install typos? [y/N]: "; read yn < /dev/tty
|
||
case $yn in
|
||
[Yy]* )
|
||
install_typos
|
||
break
|
||
;;
|
||
[Nn]* | "" )
|
||
exit 2
|
||
;;
|
||
* )
|
||
echo $invalid
|
||
;;
|
||
esac
|
||
done
|
||
fi
|
||
fi
|
||
|
||
echo -e "$Checking spelling mistakes…"
|
||
|
||
if [[ $verbose -eq 1 ]]; then
|
||
echo ""
|
||
typos --version
|
||
echo ""
|
||
fi
|
||
|
||
staged_files=`git diff --name-only --cached`
|
||
|
||
if ! typos --color always ${staged_files}; then
|
||
echo -e " Checking spelling mistakes result: $fail"
|
||
echo "Please fix the above issues, either manually or by running: typos -w"
|
||
exit 1
|
||
else
|
||
echo -e " Checking spelling mistakes result: $ok"
|
||
fi
|
||
}
|
||
|
||
# Check if files in POTFILES.in are correct.
|
||
#
|
||
# This checks, in that order:
|
||
# - All files exist
|
||
# - All files with translatable strings are present and only those
|
||
# - Files are sorted alphabetically
|
||
#
|
||
# This assumes the following:
|
||
# - POTFILES is located at 'po/POTFILES.in'
|
||
# - UI (Glade) files are located in 'data/resources/ui' and use 'translatable="yes"'
|
||
# - Rust files are located in 'src' and use '*gettext' methods or macros
|
||
check_potfiles() {
|
||
echo -e "$Checking po/POTFILES.in…"
|
||
|
||
local ret=0
|
||
|
||
# Check that files in POTFILES exist.
|
||
while read -r line; do
|
||
if [[ -n $line && ${line::1} != '#' ]]; then
|
||
if [[ ! -f $line ]]; then
|
||
echo -e "$error File '$line' in POTFILES.in does not exist"
|
||
ret=1
|
||
fi
|
||
if [[ ${line:(-3):3} == '.ui' ]]; then
|
||
ui_potfiles+=($line)
|
||
elif [[ ${line:(-3):3} == '.rs' ]]; then
|
||
rs_potfiles+=($line)
|
||
fi
|
||
fi
|
||
done < po/POTFILES.in
|
||
|
||
if [[ ret -eq 1 ]]; then
|
||
echo -e " Checking po/POTFILES.in result: $fail"
|
||
echo "Please fix the above issues"
|
||
exit 1
|
||
fi
|
||
|
||
# Get UI files with 'translatable="yes"'.
|
||
ui_files=(`grep -lIr 'translatable="yes"' data/resources/ui/*`)
|
||
|
||
# Get Rust files with regex 'gettext(_f)?\(', except `src/i18n.rs`.
|
||
rs_files=(`grep -lIrE 'gettext(_f)?\(' --exclude=i18n.rs src/*`)
|
||
|
||
# Get Rust files with macros, regex 'gettext!\('.
|
||
rs_macro_files=(`grep -lIrE 'gettext!\(' src/*`)
|
||
|
||
# Remove common files
|
||
to_diff1=("${ui_potfiles[@]}")
|
||
to_diff2=("${ui_files[@]}")
|
||
diff
|
||
ui_potfiles=("${to_diff1[@]}")
|
||
ui_files=("${to_diff2[@]}")
|
||
|
||
to_diff1=("${rs_potfiles[@]}")
|
||
to_diff2=("${rs_files[@]}")
|
||
diff
|
||
rs_potfiles=("${to_diff1[@]}")
|
||
rs_files=("${to_diff2[@]}")
|
||
|
||
potfiles_count=$((${#ui_potfiles[@]} + ${#rs_potfiles[@]}))
|
||
if [[ $potfiles_count -eq 1 ]]; then
|
||
echo ""
|
||
echo -e "$error Found 1 file in POTFILES.in without translatable strings:"
|
||
ret=1
|
||
elif [[ $potfiles_count -ne 0 ]]; then
|
||
echo ""
|
||
echo -e "$error Found $potfiles_count files in POTFILES.in without translatable strings:"
|
||
ret=1
|
||
fi
|
||
for file in ${ui_potfiles[@]}; do
|
||
echo $file
|
||
done
|
||
for file in ${rs_potfiles[@]}; do
|
||
echo $file
|
||
done
|
||
|
||
let files_count=$((${#ui_files[@]} + ${#rs_files[@]}))
|
||
if [[ $files_count -eq 1 ]]; then
|
||
echo ""
|
||
echo -e "$error Found 1 file with translatable strings not present in POTFILES.in:"
|
||
ret=1
|
||
elif [[ $files_count -ne 0 ]]; then
|
||
echo ""
|
||
echo -e "$error Found $files_count files with translatable strings not present in POTFILES.in:"
|
||
ret=1
|
||
fi
|
||
for file in ${ui_files[@]}; do
|
||
echo $file
|
||
done
|
||
for file in ${rs_files[@]}; do
|
||
echo $file
|
||
done
|
||
|
||
let rs_macro_count=$((${#rs_macro_files[@]}))
|
||
if [[ $rs_macro_count -eq 1 ]]; then
|
||
echo ""
|
||
echo -e "$error Found 1 Rust file that uses a gettext-rs macro, use the corresponding i18n method instead:"
|
||
ret=1
|
||
elif [[ $rs_macro_count -ne 0 ]]; then
|
||
echo ""
|
||
echo -e "$error Found $rs_macro_count Rust files that use a gettext-rs macro, use the corresponding i18n method instead:"
|
||
ret=1
|
||
fi
|
||
for file in ${rs_macro_files[@]}; do
|
||
echo $file
|
||
done
|
||
|
||
if [[ ret -eq 1 ]]; then
|
||
echo ""
|
||
echo -e " Checking po/POTFILES.in result: $fail"
|
||
echo "Please fix the above issues"
|
||
exit 1
|
||
fi
|
||
|
||
# Check sorted alphabetically
|
||
to_sort=("${potfiles[@]}")
|
||
sort
|
||
for i in ${!potfiles[@]}; do
|
||
if [[ "${potfiles[$i]}" != "${to_sort[$i]}" ]]; then
|
||
echo -e "$error Found file '${potfiles[$i]}' before '${to_sort[$i]}' in POTFILES.in"
|
||
ret=1
|
||
break
|
||
fi
|
||
done
|
||
|
||
if [[ ret -eq 1 ]]; then
|
||
echo ""
|
||
echo -e " Checking po/POTFILES.in result: $fail"
|
||
echo "Please fix the above issues"
|
||
exit 1
|
||
else
|
||
echo -e " Checking po/POTFILES.in result: $ok"
|
||
fi
|
||
}
|
||
|
||
# Check if files in data/resources/resources.gresource.xml are sorted alphabetically.
|
||
check_resources() {
|
||
echo -e "$Checking data/resources/resources.gresource.xml…"
|
||
|
||
local ret=0
|
||
|
||
# Get files.
|
||
regex="<file .*>(.*)</file>"
|
||
while read -r line; do
|
||
if [[ $line =~ $regex ]]; then
|
||
files+=("${BASH_REMATCH[1]}")
|
||
fi
|
||
done < data/resources/resources.gresource.xml
|
||
|
||
# Check sorted alphabetically
|
||
to_sort=("${files[@]}")
|
||
sort
|
||
for i in ${!files[@]}; do
|
||
if [[ "${files[$i]}" != "${to_sort[$i]}" ]]; then
|
||
echo -e "$error Found file '${files[$i]#src/}' before '${to_sort[$i]#src/}' in resources.gresource.xml"
|
||
ret=1
|
||
break
|
||
fi
|
||
done
|
||
|
||
if [[ ret -eq 1 ]]; then
|
||
echo ""
|
||
echo -e " Checking data/resources/resources.gresource.xml result: $fail"
|
||
echo "Please fix the above issues"
|
||
exit 1
|
||
else
|
||
echo -e " Checking data/resources/resources.gresource.xml result: $ok"
|
||
fi
|
||
}
|
||
|
||
# Check arguments
|
||
while [[ "$1" ]]; do case $1 in
|
||
-s | --git-staged )
|
||
git_staged=1
|
||
;;
|
||
-f | --force-install )
|
||
force_install=1
|
||
;;
|
||
-v | --verbose )
|
||
verbose=1
|
||
;;
|
||
-h | --help )
|
||
show_help
|
||
exit 0
|
||
;;
|
||
*)
|
||
show_help >&2
|
||
exit 1
|
||
esac; shift; done
|
||
|
||
if [[ $git_staged -eq 1 ]]; then
|
||
staged_files=`git diff --name-only --cached`
|
||
if [[ -z $staged_files ]]; then
|
||
echo -e "$Failed to check files because none where staged"
|
||
exit 2
|
||
fi
|
||
else
|
||
staged_files=""
|
||
fi
|
||
|
||
|
||
# Run
|
||
check_cargo
|
||
echo ""
|
||
run_rustfmt
|
||
echo ""
|
||
run_typos
|
||
echo ""
|
||
check_potfiles
|
||
echo ""
|
||
if [[ $git_staged -eq 1 ]]; then
|
||
staged_files=`git diff --name-only --cached | grep data/resources/resources.gresource.xml`
|
||
if [[ -z $staged_files ]]; then
|
||
check_resources
|
||
fi
|
||
else
|
||
check_resources
|
||
fi
|
||
echo ""
|