#!/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 | xargs ls -d 2>/dev/null | grep '.rs$' `
result = 0
for file in ${ staged_files [@] } ; do
if ! cargo +nightly fmt -- --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 | xargs ls -d 2>/dev/null`
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.in 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
# Check that files in POTFILES.skip exist.
while read -r line; do
if [ [ -n $line && ${ line : : 1 } != '#' ] ] ; then
if [ [ ! -f $line ] ] ; then
echo -e " $error File ' $line ' in POTFILES.skip does not exist "
ret = 1
fi
if [ [ ${ line : (-3) : 3 } = = '.ui' ] ] ; then
ui_skip += ( $line )
elif [ [ ${ line : (-3) : 3 } = = '.rs' ] ] ; then
rs_skip += ( $line )
fi
fi
done < po/POTFILES.skip
if [ [ ret -eq 1 ] ] ; then
echo -e " Checking po/POTFILES.skip 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)?\('.
rs_files = ( ` grep -lIrE 'gettext(_f)?\(' src/*` )
# Get Rust files with macros, regex 'gettext!\('.
rs_macro_files = ( ` grep -lIrE 'gettext!\(' src/*` )
# Remove common files
to_diff1 = ( " ${ ui_skip [@] } " )
to_diff2 = ( " ${ ui_files [@] } " )
diff
ui_skip = ( " ${ to_diff1 [@] } " )
ui_files = ( " ${ to_diff2 [@] } " )
to_diff1 = ( " ${ ui_potfiles [@] } " )
to_diff2 = ( " ${ ui_files [@] } " )
diff
ui_potfiles = ( " ${ to_diff1 [@] } " )
ui_files = ( " ${ to_diff2 [@] } " )
to_diff1 = ( " ${ rs_skip [@] } " )
to_diff2 = ( " ${ rs_files [@] } " )
diff
rs_skip = ( " ${ to_diff1 [@] } " )
rs_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
}
# Install cargo-sort with cargo.
install_cargo_sort( ) {
echo -e " $Installing cargo-sort… "
cargo install cargo-sort
if ! cargo-sort --version >/dev/null 2>& 1; then
echo -e " $Failed to install cargo-sort "
exit 2
fi
}
# Run cargo-sort to check if Cargo.toml is sorted.
run_cargo_sort( ) {
if ! cargo-sort --version >/dev/null 2>& 1; then
if [ [ $force_install -eq 1 ] ] ; then
install_cargo_sort
elif [ ! -t 1 ] ; then
echo "Unable to check Cargo.toml sorting, because cargo-sort could not be run"
exit 2
else
echo "Cargo-sort is needed to check the sorting in Cargo.toml, but it isn’t available"
echo ""
echo "y: Install cargo-sort via cargo"
echo "N: Don't install cargo-sort and abort checks"
echo ""
while true; do
echo -n "Install cargo-sort? [y/N]: " ; read yn < /dev/tty
case $yn in
[ Yy] * )
install_cargo_sort
break
; ;
[ Nn] * | "" )
exit 2
; ;
* )
echo $invalid
; ;
esac
done
fi
fi
echo -e " $Checking Cargo.toml sorting… "
if [ [ $verbose -eq 1 ] ] ; then
echo ""
cargo-sort --version
echo ""
fi
if ! cargo-sort --check --grouped --order package,lib,profile,features,dependencies,target,dev-dependencies,build-dependencies; then
echo -e " Cargo.toml sorting result: $fail "
echo "Please fix the Cargo.toml file, either manually or by running: cargo-sort --grouped --order package,lib,profile,features,dependencies,target,dev-dependencies,build-dependencies"
exit 1
else
echo -e " Cargo.toml sorting 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 | xargs ls -d 2>/dev/null | grep data/resources/resources.gresource.xml`
if [ [ -z $staged_files ] ] ; then
check_resources
fi
else
check_resources
fi
echo ""
run_cargo_sort
echo ""