Skip to content

Instantly share code, notes, and snippets.

@Fordi
Last active December 29, 2020 18:51
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Fordi/29b8d6d1ef1662b306bfc2bd99151b07 to your computer and use it in GitHub Desktop.
Save Fordi/29b8d6d1ef1662b306bfc2bd99151b07 to your computer and use it in GitHub Desktop.
Get the current hash from a Git repository without external dependencies (including Git)
#!/bin/sh
# From the Stack response:
# http://stackoverflow.com/a/33133769/353872
#
# Also acts as a low-complexity example of how to flexibly parse the argument
# list in raw sh without external programs or a lot of fuss.
# For use in usage() and in log messages
SCRIPT_NAME="$(basename $0)"
# Usage function: tells the user what's up. ALWAYS implement this.
function usage() {
# &2 === stderr; separates information for the user from
# data for a consuming script
echo "Get HASH of Git repositories at current branch and revision" >&2
# The
echo "Usage: $SCRIPT_NAME [-v] PATH [PATH...]" >&2
echo " -v|--verbose Print a log of the references as they are followed" >&2
echo " PATH Paths for which to get revision hash" >&2
}
VERBOSE=0
# Create an array for non-flag arguments
ARGS=()
# Loop over arguments; we'll be shifting the list as we go,
# so we keep going until $1 is empty
while [ -n "$1" ]; do
# Capture and shift the argument.
ARG="$1"
shift
case "$ARG" in
# User requested help; sometimes they do this at the end of a command
# while they're building it. By capturing and exiting, we avoid doing
# work before it's intended.
-h|-\?|-help|--help)
usage
exit 0
;;
# Make the script more verbose
# This script only has levels '0' and '1', so the math is unnecessary
# (we could go with "" and "yes" or some other simple binary), but
# this would support more complex levels of verbosity/quietness
-v|--verbose)
VERBOSE=$((VERBOSE + 1))
;;
# Make the script quieter
-q|--quiet)
VERBOSE=$((VERBOSE - 1))
;;
# All arguments that follow are non-flags
# This should be in all of your scripts, to more easily support filenames
# that start with hyphens. Break will bail from the `for` loop above.
--)
break
;;
# Something that looks like a flag, but is not; report an error and die
-?*)
echo "$SCRIPT_NAME Unknown option: '$ARG'" >&2
usage
exit 1
;;
#
# All other arguments are added to the ARGS array.
*)
ARGS=(${ARGS[@]} "$ARG")
;;
# An alternate implementation, if your script does not accept unflagged
# arguments, is:
# *)
# echo "$SCRIPT_NAME Invalid argument: '$ARG'" >&2
# usage
# exit 1
# ;;
esac
done
# If the above script found a '--' argument, there will still be items in $*;
# move them into ARGS
while [ -n "$1" ]; do
ARGS=(${ARGS[@]} "$1")
shift
done
# For this script, you must specify at least one item that ends up in ARGS;
# Report an error, print usage, and die
if [ ${#ARGS[@]} == 0 ]; then
echo "Please specify a repository" >&2
usage
exit 1
fi
# Loop through the values of ARGS
# This is the correct way to loop through a shell array so that whitespace in
# the arguments is preserved; if you looped with `for REPO in $ARGS`, you'd get
# your REPOs split by spaces - which is almost certainly not what you want to
# do.
for REPO in "${ARGS[@]}"; do
# Store the user's working directory, then change to the requested directory
WD="$PWD"
cd "$REPO"
# Check for the presence of a directory named '.git' If it's not there,
# report the error and die
if [ ! -d ".git" ]; then
echo "$REPO is not a Git repository" >&2
exit 1
fi
# Git's structure is relatively simple. `.git/HEAD` contains a reference to
# the location of the current HEAD pointer. References are formatted as
# "ref: {path}", where `path` is relative to `.git`. If `ref:` is not present,
# the string is the hash, representing a path relative to .git/objects
# (actually, {path:0:2}/{path:2:}), to the packed delta of the latest version
# of the object (which could be a file or directory or whatever).
# For this script, we only care about refs and the hash itself, not the content
# of objects
# Start with a pseudo-reference to HEAD
HASH="ref: HEAD";
# As long as HASH starts with "ref:" (which is illegal for hashes)
while [ "${HASH:0:4}" == "ref:" ]; do
# Capture the HASH
REF="${HASH:5}"
if [ $VERBOSE -gt 0 ]; then
echo "Following reference chain to $REF" >&2
fi
if [ ! -f ".git/$REF" ]; then
echo "Failed to follow reference: '.git/$REF'! This implies that " >&2
echo "this Git repository is broken!" >&2
exit 1
fi
HASH="$(cat ".git/$REF")"
done
# Print the final value of HASH, which should be the repository's HEAD hash
echo $HASH
# Restore the working directory, so that the next call to `cd` is relative
# to the user's working path
cd "$WD"
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment