Skip to content

Instantly share code, notes, and snippets.

@drydenp
Last active May 25, 2017 14:50
Show Gist options
  • Save drydenp/83116f5149a20a4ea25b7e5c98dd883a to your computer and use it in GitHub Desktop.
Save drydenp/83116f5149a20a4ea25b7e5c98dd883a to your computer and use it in GitHub Desktop.
To be run from a cron job to create automatic commits for a git repository that is used to house subrepositories
#!/bin/sh
unset int
[ -t 0 ] && int=yes
medium_minus_date="commit %H%nAuthor: %an <%ae>%n%n%w(0,4,4)%B"
csi=$(printf "\033["); w=${csi}1m; n=${csi}m
[ $# -eq 0 -o "$1" = "--help" ] && {
cat << EOF | less -r
-------------------------------------------------------------------------------------------------
$w$(basename "$0")$n
-------------------------------------------------------------------------------------------------
This script will look in the supplied directory for a git repository that has submodules that
are in need of an update in the parent.
In other words it serves to bring the parent up-to-date with the latest commits in the children.
These commits are recorded in the .gitmodules file and point to a commit in the children.
This tool will merely create commits for modified children which will update .gitmodules and
will also allow you to push it to the server if you have one configured.
USAGE:
$(basename $0) [-p] <directory>
WHERE:
-p will attempt a push when all commits are successful.
You must configure any password using a credential helper, such as
credential-store or credential-cache with a minimum timeout of 3600
seconds.
Use "git config --global credential.helper 'cache --timeout 3600'" to
configure it.
<directory> is the directory of the parent git repository that houses the children
children are referenced by their configured path.
This script is meant to be run as a cron script every hour. It will email any results to the
local user it is running as with user@FQDN as derived from hostname -f.
Failure to commit and failure to push are emailed to the user. Any other errors are output to
cron. The script will not proceed if there are staged changes (the index is dirty), this will
also be emailed to you. If run interactively, progress messages will be printed. This script
works with Bash and Busybox Ash. When pushing without a sufficient helper defined, or a cache
has timed out, an empty password will be sent and the server error will be in the commit email
you get.
EOF
exit 255
}
unset push
unset repository
while [ $# -gt 0 ]; do
case $1 in
-p) push=true; ;;
*) repository=$1; ;;
esac
shift
done
[ -z "$repository" ] && {
echo "You need to supply a repository on the command line." >&2
exit 255
}
cd "$repository" && {
repository=$(readlink -f "$(pwd)")
} || {
echo "Directory $repository does not exist or is not accessable" >&2
exit 1
}
[ -d .git ] || {
echo "This is not a git repository: $repository" >&2
exit 1
}
# get-url was not implemented in older git versions
# We shall first get the list of submodules using:
submodules=$(git submodule -q foreach 'echo $path')
[ -t 0 ] && { echo "List of submodules:"; echo "$submodules" | sed "s/.*/ &/"; echo; }
# Then we shall hopefully find the list of modified submodules using:
out=$(git status --porcelain --ignore-submodules=dirty) || {
echo "Git status gave an error. Is this a git repository?" >&2
exit 1
}
list=$(printf "%s\n" "$out" | sed "s/.*/#&/" | while read a; do
# Spaces as meaningful characters is not easy in shell code.
c=$(echo "$a" | sed "s/#.. //")
b=${a% $c}
b=${b#\#}
# This first part filters on subrepository names.
if echo "$submodules" | grep -q "^$c$"; then
printf "%-2s %s\n" "$b" "$c"
fi
# Then we filter on an "M" in the second column.
done | grep "^.M" | sed "s/.. //" )
[ -t 0 ] && {
[ -n "$list" ] && {
echo "Submodules that are modified:"
echo "$list" | sed "s/.*/ &/"
echo
} || {
echo "There are no modified submodules."
echo
}
}
# Then we shall go on.
# I used to use the human readable version but it didn't tell me whether something was a submodule
# when it already had the submodule staged for commit....:
# out=$(echo "$out" | grep "^# modified:.*(.*new commits.*)$" | sed "s/.*: *\([^ ].*\) (.*)$/\1/")
[ -z "$list" ] && {
exit 0
} || {
username=$(id -nu)
hostname=$(hostname -f)
email="$username@$hostname"
repo_name=$(git remote show -n origin | grep "Fetch URL" | sed "s@.*/\(.*\).git@\1@")
# Now we check whether the index does not contain staged changes:
git status --porcelain --ignore-submodules=dirty | grep -q "^[MADRC]." && {
# And we send an email if they are staged:
echo "From: $email
To: $email
Subject: Repo $repo_name already contains staged changes
There are sub-modules that have changes to be committed to the parent repo $repo_name but the index is not clean; there are staged changes:
$(git status --ignore-submodules=dirty | sed -n -e "/^# Changes to be committed/,/^# [^ ]/p" | sed '$d')" | /usr/sbin/sendmail -t
[ -t 0 ] && echo "Sent staged changes preventing operation mail to $email"
# And we exit. Stash -u would take care of those staged things but unfortunately I don't think it is very
# safe to do so.
# I can't tell if I can ever lock a repository and files might also be open in the working tree that then
# gets swapped around back and forth.
# I would have to unstage things and then restage them back. That's easy enough but still runs the potential
# risk of interfering with a user.
# What I do needs to be atomic etc.
exit 1
}
# Let's first determine the date to use for our commit message:
# The date is slightly influenced by the locale (the %a term means like Wed)
# This string results in "Wed 31-10-2017 22:00". I didn't like my own locale
# (en_US becomes 10/31/2017 but nl_NL becomes 31-10-17 and locales also change dots into commas.
# Very annoying. I hate that so much. Calculators don't take . input, only , input. So the thing
# you're used to, typing 10.23 as a decimal number, suddenly doesn't work and the number becomes
# 1023. That person who invented that deserves a proper hanging.
date=$(date "+%a %d-%m-%Y %H:%M")
# Anyway you can always change this around to your preference.
# The list is now supplied with a here doc down below so that we can save the variables inside.
unset error_repos errmsg changelist
while read a; do
git add "$a"
currerr=$(git commit -m "Hourly repository update for submodule $a at $date" 2>&1 > /dev/null) && {
changelist="$changelist$a
"
[ $int ] && echo "Successfully committed $a."
true
} || {
error=$?
error_repos="$error_repos$a
"
errmsg="$errmsg$currerr
"
[ $int ] && echo "Failed to commit $a."
git reset -q "$a"
}
done << EOF
$list
EOF
# Push if requested, but only if no error occurred.
if [ -n "$push" -a -z "$error_repos" ]; then
# Now if there is no credential helper configured we have a problem.
# We are going to assume there is either a cache or a store option configured.
# If this program runs, then ideally in the last hour we have had a commit
# by a normal person. Thus, even a cache with a timeout of 3600 seconds would be
# sufficient.
# I have now verified that the below will prevent Git from asking for a password
# and use an empty password instead. This will make the remote origin bolt of course.
# You then get an error message that the push failed but this should ordinarily
# not happen if you have a cache of at least 3600 seconds.
[ -t 0 ] && echo "Pushing result(s) in one go."
push_result=$(GIT_ASKPASS=/bin/true git push 2>&1) && {
push_error=0
[ -t 0 ] && echo "Push succeeded."
} || {
push_error=$?
[ -t 0 ] && echo "Push failed."
}
# So what happens is that your credential helper is used if it exists and otherwise
# and empty password is used which will cause the push to fail.
# You are responsible for giving credentials to your pushes. I am not going to introduce
# another git password mechanism.
# Of course it would be possible though to configure something on the command line
# or reading a file from disk.
# This thing will obviously just push the currently checked-out repository (branch).
# And it will push it to its remote tracking branch, which would ordinarily be origin?
# If you need more, you will have to modify this (feature creep!).
fi
# Remove the extra \n I gave it:
changelist=${changelist%?}
error_repos=${error_repos%?}
errmsg=${errmsg%?}
# If there were errors at committing they are now collected in this error repo list.
[ -n "$error_repos" ] && {
{
# Start and error commits:
echo "From: $email
To: $email
Subject: Commits contain errors @ auto-update for $repo_name
Commits that gave errors:
$(echo -n "$error_repos" | sed "s/.*/- &/")"
# Any commits that succeeded:
if [ -n "$changelist" ]; then
echo "
Commits that succeeded:
$(echo -n "$changelist" | sed "s/.*/- &/")"
fi
# The error messages:
echo "
Error messages:
$(echo -n "$errmsg" | sed "s/.*/ &/")"
if [ -n "$changelist" -a -n "$push" ]; then
echo "
The commits that succeeded have not been pushed."
fi
} | /usr/sbin/sendmail -t
[ -t 0 ] && echo "Sent error mail to $email"
exit 1
}
# If not, then we proceed with a regular commit message as there MUST be successful commits now:
count=$(echo "$changelist" | wc -l)
if [ "$count" -eq 1 ]; then
subject="Updated parent for sub-module $repo_name/$changelist"
else
subject="Updated parent $repo_name for $count submodule repositories"
fi
{
echo "From: $email
To: $email
Subject: $subject
"
while read r; do
echo "The repository $repo_name had its reference to submodule $r updated to its latest commit:
$(cd $r; git log -1 --format="$medium_minus_date" | sed "s/.*/ &/")
"
done << EOF
$changelist
EOF
if [ "$push" ]; then
echo -n "Push was requested and "
[ $push_error -eq 0 ] && echo -n "succeeded" || echo -n "failed"
echo ".
Push message is as follows:
$(echo "$push_result" | sed "s/.*/ &/")"
fi
} | /usr/sbin/sendmail -t
[ -t 0 ] && echo "Sent commit mail to $email"
true
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment