Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve format-incremental scripts #996

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
249 changes: 157 additions & 92 deletions check/format-incremental
Original file line number Diff line number Diff line change
@@ -1,112 +1,177 @@
#!/usr/bin/env bash
# Summary: check files against style guidelines and optionally reformat them.
# Run this program with the argument --help for usage information.

################################################################################
# Formats python files that have been modified.
#
# Usage:
# check/format-incremental [BASE_REVISION] [--apply] [--all]
#
# By default, the script analyzes python files that have changed relative to the
# base revision and determines whether they need to be formatted. If any changes
# are needed, it prints the diff and exits with code 1, otherwise it exits with
# code 0.
#
# With '--apply', reformats the files instead of printing the diff and exits
# with code 0.
#
# With '--all', analyzes all python files, instead of only changed files.
#
# You can specify a base git revision to compare against (i.e. to use when
# determining whether or not a file is considered to have "changed"). For
# example, you can compare against 'origin/master' or 'HEAD~1'.
#
# If you don't specify a base revision, the following defaults will be tried, in
# order, until one exists:
#
# 1. upstream/master
# 2. origin/master
# 3. master
#
# If none exists, the script fails.
################################################################################

# Get the working directory to the repo root.
read -r -d '' usage <<-EOF
Usage:

${0##*/} [BASE_REV] [--help] [--apply] [--all] [--no-color] [--quiet]

Check the format of Python source files against project style guidelines. If
any changes are needed, this program prints the differences to stdout and exits
with code 1; otherwise, it exits with code 0.

Main options
~~~~~~~~~~~~

If the option '--apply' is supplied as an argument, then instead of printing
differences, this program reformats the files and exits with code 0 if
successful or 1 if an error occurs.

By default, this program examines only those files that git reports to have
changed in relation to the git revision (see next paragraph). With option
'--all', this program will examine all files instead of only the changed files.

File changes are considered relative to the base git revision in the repository
unless a different git revision is given as an argument to this program. The
revision can be given as a SHA value or a name such as 'origin/main' or
'HEAD~1'. If no git revision is provided as an argument, this program tries the
following defaults, in order, until one is found to exist:

1. upstream/main (or upstream/master)
2. origin/main (or origin/master)
3. main (or master)

If none of them exists, the program will fail and return exit code 1.

Additional options
~~~~~~~~~~~~~~~~~~

Informative messages are printed to stdout unless option '--quiet' is given.
(Error messages are always printed.)

Color is used to enhance the output unless the option '--no-color' is given.

Running this program with the option '--help' will make it print this help text
and exit with exit code 0 without doing anything else.

If an error occurs in Black itself, this program will return the non-zero error
code returned by Black.
EOF

# Change the working directory of this script to the root of the repo.
thisdir="$(dirname "${BASH_SOURCE[0]}")" || exit $?
topdir="$(git -C "${thisdir}" rev-parse --show-toplevel)" || exit $?
cd "${topdir}" || exit $?


# Parse arguments.
only_print=1
only_changed=1
rev=""
for arg in "$@"; do
if [[ "${arg}" == "--apply" ]]; then
only_print=0
elif [[ "${arg}" == "--all" ]]; then
only_changed=0
elif [ -z "${rev}" ]; then
if [ "$(git cat-file -t "${arg}" 2> /dev/null)" != "commit" ]; then
echo -e "\033[31mNo revision '${arg}'.\033[0m" >&2
cd "$(git -C "${thisdir}" rev-parse --show-toplevel)" || exit $?

# Set default values.
declare only_print=true
declare only_changed=true
declare no_color=false
declare be_quiet=false

function print() {
local type="$1" msg="$2"
local red="" green="" reset=""
$no_color || red="\033[31;1m"
$no_color || green="\033[32;1m"
$no_color || reset="\033[0m"
case $type in
error) echo -e "${reset}${red}Error: $msg${reset}" >&2;;
info) $be_quiet || echo -e "${reset}${green}$msg${reset}";;
*) echo "$msg";;
esac
}

declare rev=""

# Parse the command line.
# Don't be fussy about whether options are written upper case or lower case.
shopt -s nocasematch
while (( $# > 0 )); do
case $1 in
-h | --help)
echo "$usage"
exit 0
;;
--apply)
only_print=false
shift
;;
--all)
only_changed=false
shift
;;
--no-color)
no_color=true
shift
;;
--quiet)
be_quiet=true
shift
;;
-*)
print error "Unrecognized option $1."
echo "$usage"
exit 1
fi
rev="${arg}"
else
echo -e "\033[31mToo many arguments. Expected [revision] [--apply] [--all].\033[0m" >&2
exit 1
fi
;;
*)
if [[ -n "$rev" ]]; then
print error "Too many arguments."
echo "$usage"
exit 1
fi
if ! git rev-parse -q --verify --no-revs "$1^{commit}"; then
print error "Cannot find revision $1."
exit 1
fi
rev="$1"
shift
;;
esac
done
shopt -u nocasematch

typeset -a format_files
if (( only_changed == 1 )); then
# Gather a list of Python files that have been modified, added, or moved.
declare -a modified_files=("")
if $only_changed; then
# Figure out which branch to compare against.
if [ -z "${rev}" ]; then
if [ "$(git cat-file -t upstream/master 2> /dev/null)" == "commit" ]; then
rev=upstream/master
elif [ "$(git cat-file -t origin/master 2> /dev/null)" == "commit" ]; then
rev=origin/master
elif [ "$(git cat-file -t master 2> /dev/null)" == "commit" ]; then
rev=master
else
echo -e "\033[31mNo default revision found to compare against. Argument #1 must be what to diff against (e.g. 'origin/master' or 'HEAD~1').\033[0m" >&2
if [[ -z "$rev" ]]; then
declare -r -a try=("upstream/main" "origin/main" "main"
"upstream/master" "origin/master" "master")
for name in "${try[@]}"; do
if [[ "$(git cat-file -t "$name" 2> /dev/null)" == "commit" ]]; then
rev="$name"
break
fi
done
if [[ -z "$rev" ]]; then
print error "None of the defaults (${try[*]}) were found and no" \
" git revision was provided as argument. Argument #1 must" \
" be what to diff against (e.g., 'origin/main' or 'HEAD~1')."
exit 1
fi
fi
base="$(git merge-base "${rev}" HEAD)"
if [ "$(git rev-parse "${rev}")" == "${base}" ]; then
echo -e "Comparing against revision '${rev}'." >&2
else
echo -e "Comparing against revision '${rev}' (merge base ${base})." >&2
rev="${base}"
declare base base_info
base="$(git merge-base "$rev" HEAD)"
if [[ "$(git rev-parse "$rev")" != "$base" ]]; then
rev="$base"
base_info=" (merge base $base)"
fi
print info "Comparing files to revision '$rev'$base_info."

# Get the modified, added and moved python files.
IFS=$'\n' read -r -d '' -a format_files < \
<(git diff --name-only --diff-filter=MAR "${rev}" -- '*.py' ':(exclude)*_pb2.py')
# Get the list of changed files.
IFS=$'\n' read -r -d '' -a modified_files < \
<(git diff --name-only --diff-filter=MAR "$rev" -- '*.py')
else
echo -e "Formatting all python files." >&2
IFS=$'\n' read -r -d '' -a format_files < \
<(git ls-files '*.py' ':(exclude)*_pb2.py')
# The user asked for all files.
print info "Formatting all Python files."
IFS=$'\n' read -r -d '' -a modified_files < <(git ls-files '*.py')
fi

if (( ${#format_files[@]} == 0 )); then
echo -e "\033[32mNo files to format\033[0m."
if (( ${#modified_files[@]} == 0 )); then
print info "No modified files found – no changes needed."
exit 0
fi

BLACKVERSION="$(black --version)"

echo "Running the black formatter... (version: $BLACKVERSION)"
declare black_version
black_version="$(black --version)"
black_version=${black_version//[$'\n']/ } # Remove annoying embedded newline.
black_version=${black_version#black, } # Remove leading "black, "
print info "Running Black (version $black_version) ..."

args=("--color")
if (( only_print == 1 )); then
args+=("--check" "--diff")
fi

black "${args[@]}" "${format_files[@]}"
BLACKSTATUS=$?
declare -a black_args
$only_print && black_args+=("--check" "--diff")
$be_quiet && black_args+=("--quiet")
$no_color && black_args+=("--no-color")

if [[ "$BLACKSTATUS" != "0" ]]; then
exit 1
fi
exit 0
black "${black_args[@]}" "${modified_files[@]}"
Loading