Created
April 10, 2024 20:37
-
-
Save jamesonwilliams/27435255ba543c9b9f407b1a61d750ca to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# Print a message and kill the script. | |
die() { | |
echo "$@" 1>&2 | |
exit 1 | |
} | |
# Finds the top of the repo. | |
find_git_repo_top() { | |
local current_dir=$(pwd) | |
# Loop until reaching the root directory "/" | |
while [ "$current_dir" != "/" ]; do | |
# Check if ".git" directory exists | |
if [ -d "$current_dir/.git" ]; then | |
echo "$current_dir" | |
return | |
fi | |
# Move up one directory | |
current_dir=$(dirname "$current_dir") | |
done | |
# If ".git" directory is not found | |
echo "Git repository not found." | |
exit 1 | |
} | |
# Ask the user a yes/no question and await their response. Return 0 if | |
# they say yes (in some format). | |
await_yes_no() { | |
read -r answer | |
case "$answer" in | |
[yY]|[yY][eE][sS]) | |
echo 0 | |
;; | |
*) | |
echo 1 | |
;; | |
esac | |
} | |
# Delete whatever hooks may be active in the .git/hooks directory. This | |
# may include things like the old pre-commit hook we had been using | |
# prior for April, 2024. | |
delete_existing_hooks_with_confirmation() { | |
project_root="$1" | |
hooks=$(find "${project_root}/.git/hooks/" -mindepth 1 ! -name "*.sample") | |
echo "Found hook files: $hooks" | |
echo "OK to delete? [Y/n]" | |
if [ "$(await_yes_no)" -ne 0 ]; then | |
die "OK; aborting." | |
fi | |
rm -f -r $hooks | |
} | |
# Install the pre-push script and any hooks found under the ./scripts | |
# directory. | |
install_pre_push_hooks() { | |
project_root="$1" | |
echo "Installing scripts into .git/hooks ..." | |
mkdir -p "${project_root}/.git/hooks" | |
ln -s "${project_root}/scripts/pre-push" "${project_root}/.git/hooks/pre-push" | |
} | |
# Installs pre-push scripts after ensuring we're running from the | |
# directory root, and after cleaning up any old git hook scripts. | |
main() { | |
project_root="$(find_git_repo_top)" | |
delete_existing_hooks_with_confirmation "$project_root" | |
install_pre_push_hooks "$project_root" | |
} | |
main |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# | |
# This script looks for Kotlin files that have been changed locally and | |
# ensures that they are correctly formatted according to ktfmt. This | |
# script makes an effort to only run on the smallest possible set of | |
# changed files as opposed to running broadly over the codebase, to | |
# improve pre-push performance. | |
# | |
# The script takes the following steps: | |
# | |
# 1. Figure out the name of the remote for your repo on | |
# GitHub. If there is no remote (via `git remote`) add one called | |
# "your_company" | |
# 2. Fetch the latest `main` ref from that remote. | |
# 3. For any commits that are about to be pushed (git push can push | |
# multiple references at once), do the following steps, 4-8: | |
# 4. Find an ancestor commit that is before both origin/main and the | |
# commit you're trying to push. | |
# 5. Compute a list of .kt or .kts files that have changed between that | |
# ancestor and the commit being pushed | |
# 6. Run ./gradlew ktfmtFormatPartial on only those files using --run-over=<list>. | |
# 7. If there are no formatting changes applied, proceed to push. | |
# Otherwise, print a descriptive error message noting that files | |
# have been formatted and that the user will need to commit manually | |
# and push. | |
# Print the name of the configured remote (usually "origin") | |
expected_remote() { | |
git remote -v | awk '/[email protected]:Your-org\/your-repo.git \(fetch\)/ { print $1 }' | |
} | |
# Returns the name of the your_company origin. If it is not found locally, | |
# we'll add one called "your_company" (conservative name so it doesn't clash | |
# with whatever else you have going on.) | |
ensure_remote_installed() { | |
remote_name="$(expected_remote)" | |
if [ -z "$remote_name" ]; then | |
git remote add your_company "[email protected]:Your-org/your-repo.git" | |
echo "your_company" | |
else | |
echo "$remote_name" | |
fi | |
} | |
# Fetches, but does not apply, the remote references from GitHub's copy | |
# of the project. | |
fetch_remote_refs() { | |
remote_name="$1" | |
git fetch "$remote_name" main &>/dev/null | |
} | |
# Computes a list of the names of the files that have changed between | |
# two commit hashes. | |
compute_changed_files() { | |
from_hash="$1" | |
to_hash="$2" | |
git diff --name-only "$from_hash" "$to_hash" | |
} | |
# Determines if a file is a kotlin file. | |
is_kotlin_file() { | |
file_path="$1" | |
if [[ "$file_path" =~ .kts?$ ]]; then | |
echo 0 | |
else | |
echo 1 | |
fi | |
} | |
# Computes which .kts? files have changed. | |
compute_changed_kotlins() { | |
from_hash="$1" | |
to_hash="$2" | |
changed_kotlins="" | |
for changed_file in $(compute_changed_files "$from_hash" "$to_hash"); do | |
if [ "$(is_kotlin_file $changed_file)" -eq 0 ]; then | |
changed_kotlins="$changed_file $changed_kotlins" | |
fi | |
done | |
echo "$changed_kotlins" | |
} | |
# This finds a common ancestor between origin/main and whatever commit | |
# is being pushed. The idea here is that the local tree may not be | |
# rebased onto origin/main itself, so we need to look backwards in | |
# origin/main to find a commit that *is* in our history. This is the | |
# developer's current marker for origin/main. | |
compute_from_hash() { | |
remote_name="$1" | |
local_hash="$2" | |
git merge-base "$remote_name/main" "$local_hash" | |
} | |
# Given two lists of strings compute the insersection of the two lists, | |
# e.g., if A="foo bar", and B="foo", the intersection is "foo". | |
compute_intersection() { | |
intersection="" | |
foo="$1" | |
bar="$2" | |
for f in $foo; do | |
for b in $bar; do | |
if [[ "$b" == "$f" ]]; then | |
intersection="$intersection $b" | |
fi | |
done | |
done | |
echo $intersection | sort | uniq | xargs | |
} | |
# Checks the result of the ktfmt task to see if formatted any files. If | |
# files were formatted, fail the hook and emit an error. If none were | |
# formatted, continue to exit the hook successfully. | |
fail_if_any_formatted() { | |
changed_since_main="$1" | |
changed_after_fmt="$(git diff --name-only | xargs)" | |
changed_in_both=$(compute_intersection "$changed_since_main" "$changed_after_fmt") | |
if [ ! -z "$changed_in_both" ]; then | |
cat <<- EOF | |
The following files were not formatted correctly and have been fixed locally. Please commit them and try your push again. | |
$changed_in_both | |
EOF | |
exit 1 | |
fi | |
} | |
# Runs ktfmt over any changed .kts? files. | |
validate_ktfmt() { | |
remote_name="$1" | |
to_hash="$2" | |
from_hash="$(compute_from_hash $remote_name $to_hash)" | |
changed_kotlins="$(compute_changed_kotlins $from_hash $to_hash | xargs)" | |
if [ -z "$changed_kotlins" ]; then | |
exit 0 | |
fi | |
echo "Running ktfmtFormatPartial over changed Kotlin files: $changed_kotlins ..." | |
./gradlew ktfmtFormatPartial --run-over="$changed_kotlins" &>/dev/null | |
fail_if_any_formatted "$changed_kotlins" | |
} | |
remote_name=$(ensure_remote_installed) | |
fetch_remote_refs "$remote_name" | |
while read localname localhash remotename remotehash; do | |
validate_ktfmt "$remote_name" "$localhash" | |
done | |
exit 0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# | |
# Don't write actual logic in this file. This file just fans out to | |
# .git/hooks/pre-push.d/<your_file>, so that we can add a number of | |
# checks into one place. This file aims to honor the original pre-push | |
# contract and fan it out to stub files. | |
# Finds the top of the repo. | |
find_git_repo_top() { | |
local current_dir=$(pwd) | |
# Loop until reaching the root directory "/" | |
while [ "$current_dir" != "/" ]; do | |
# Check if ".git" directory exists | |
if [ -d "$current_dir/.git" ]; then | |
echo "$current_dir" | |
return | |
fi | |
# Move up one directory | |
current_dir=$(dirname "$current_dir") | |
done | |
# If ".git" directory is not found | |
echo "Git repository not found." | |
exit 1 | |
} | |
project_root=$(find_git_repo_top) | |
hooks=$(find ${project_root}/scripts/pre-push.d -type f ! -name "*.sw*" | sort) | |
# pre-push.d receives four pieces of information for each source-target | |
# push that may be in play. For exmaple, origin->refs/heads/origin, and | |
# my_kooll->refs/heads/my_kool_branch would cause two iterations of this | |
# while loop. | |
while read localname localhash remotename remotehash; do | |
# For each set of push data, iterate over the hooks in alphabetical | |
# order. Pass the hook data in using the same contract. | |
for hook in $hooks; do | |
echo "$localname $localhash $remotename $remotehash" | bash "$hook" | |
RESULT="$?" | |
if [ $RESULT != 0 ]; then | |
exit "$RESULT" | |
fi | |
done | |
done | |
exit 0 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment