Last active
December 29, 2020 18:51
-
-
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)
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/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