Skip to content

Instantly share code, notes, and snippets.

@leandro-lucarella-sociomantic
Last active December 4, 2015 20:52
Show Gist options
  • Save leandro-lucarella-sociomantic/933d4b111d0fd03efba4 to your computer and use it in GitHub Desktop.
Save leandro-lucarella-sociomantic/933d4b111d0fd03efba4 to your computer and use it in GitHub Desktop.
Git submodule summary summary

git-subsum is a more concise but descriptive way to summarize the changes in your submodules, specially suitable to include in commit messages.

The basic usage is just git subsum. You can also specify a reference, like git subsum HEAD^ to see what changed in the last commit.

It always try to use descriptive names for references (tag names with the number of extra commits in a style similar to git describe) and by default it will only print the first 5 commit messages for a submodule.

The option -c will only print changes in the index (or staging area), is similar to the --cached option for git submodule summary.

The options -m and -n can be used to change the default maximum and minimum number of commits to print (respectively). If the minimum is negative, the maximum is used as minimum, which is the default. Otherwise, if a minimum is set, then if there are more commits to show than the maximum, only the minimum value is printed.

For example -m 5 -n 2 will printed:

* submodule v1.17.0+8(7618cac)...v1.18.0+6(f97f271) (58 commits)
  > Some change
  > Some other change
  (...)

But if -m 5 -n -1 is used (which is the default), this will be printed:

* submodule v1.17.0+8(7618cac)...v1.18.0+6(f97f271) (58 commits)
  > Some change
  > Some other change
  > Change 3
  > Change 4
  > Change 5
  (...)

The -m and -n parameters can be also configured in a per-repository or global/system fashion by using the git config variables subsum.max and subsum.min respectively.

There is also a prepare-commit-msg hook script available. If you copy this file to the .git/hooks/ directory, then the output of git subsum will be automatically added (and updated) when you commit something.

If you installed the Debian package, this is enabled by default for new repositories you created. If you want the old repositories to be updated, you have to go to the repository directory and run git init again.

#!/bin/sh
set -e
cd `dirname $0`
genchangelog()
{
echo "$1 ($2) `lsb_release -sc`; urgency=low"
echo
prevtag=$(git describe --abbrev=0 HEAD^ 2>/dev/null || true)
test -z "$prevtag" &&
prevtag=$(git rev-list --parents HEAD |
egrep "^[a-f0-9]{40}$" | head -n1)
git log --date=short --format=" * %s (%h, %cd)" "$prevtag"..HEAD |
fold --spaces --width 76 | sed 's/^\([^ ]\+\)/ \1/'
echo
echo " -- $3 `LANG=C date -R`"
}
pkgversion=$(git describe --dirty | cut -c2- |
sed 's/-\([0-9]\+\)-\(g[0-9a-f]\+\)/+\1~\2/' |
sed 's/\(~g[0-9a-f]\+\)-dirty$/-dirty\1/' |
sed 's/-dirty/~dirty.'`date +%Y%m%d%H%M%S`'/'
)-$(lsb_release -cs)
pkgmaint="Core Team <core-team@sociomantic.com>"
changelog=`mktemp`
trap "rm -f '$changelog'; exit 1" INT TERM QUIT
genchangelog "$pkgname" "$pkgversion" "$pkgmaint" > "$changelog"
realhook=`mktemp`
trap "rm -f '$changelog' '$realhook'; exit 1" INT TERM QUIT
cat <<EOT > $realhook
#!/bin/sh
test -x /usr/share/git-subsum/prepare-commit-msg &&
exec /usr/share/git-subsum/prepare-commit-msg "\$@"
exit 0
EOT
chmod a+rx $realhook
pkgname=git-subsum
fpm -f -s dir -t deb -n "$pkgname" -v "$pkgversion" \
--architecture all \
--maintainer "$pkgmaint" \
--description "A better git submodule summary command" \
--url 'https://gist.github.com/leandro-lucarella-sociomantic/933d4b111d0fd03efba4' \
--vendor 'Sociomantic Labs GmbH' \
--license GPLv3 \
--category vcs \
--depends bash \
--depends python2.7 \
--depends "git (>=1.7.7)" \
--deb-changelog "$changelog" \
git-subsum=/usr/bin/ \
README.md=/usr/share/doc/$pkgname/ \
prepare-commit-msg=/usr/share/git-subsum/ \
"$realhook"=/usr/share/git-core/templates/hooks/prepare-commit-msg
#!/bin/bash
set -e
is_int()
{
echo "$1" | egrep -q -- '^-?[0-9]+$'
}
is_uint()
{
echo "$1" | egrep -q -- '^[0-9]+$'
}
usage()
{
cat <<HELP
Usage: git subsum [-h|-c|-m MAX|-n MIN] [REF]
Shows the summary of changes to submodules in a more meaningful way (using
symbolic names using tags and git describe).
REF is an optional reference to use to get the summary from, used when calling
git submodule summary.
Options
-h Shows this help message and exit
-c Show only cached/staged changes to submodules
-m MAX Shows at most MAX commits messages per submodule, if there are more
than MAX commits, then it will show only MIN commit messages
(default: $max, might come from git config subsum.max)
-n MIN Shows at least MIN commit messages per submodule. This is used only if
the number of commit messages to show is bigger than MAX. If MIN is -1
then the value of MAX is used. If MIN is 0, then no commit messages are
shown at all if MAX is reached
(default: $min, might come from git config subsum.min)
HELP
}
cached=
max=`git config --int subsum.max; true`
test -z "$max" &&
max=5
min=`git config --int subsum.min; true`
test -z "$min" &&
min=-1
OPTIND=1
while getopts "hcm:n:" o; do
case "$o" in
m)
max=${OPTARG}
;;
n)
min=${OPTARG}
;;
c)
cached=--cached
;;
h)
usage
exit 0;
;;
*)
usage >&2
exit 2;
;;
esac
done
# More validations
is_uint "$max" || {
usage >&2
exit 2
}
is_int "$min" || {
usage >&2
exit 2
}
test $min -lt 0 &&
min=$max
shift $(($OPTIND-1)) && true
ref="$1"
shift && true
describe()
{
m=$1 ; shift
h=$1 ; shift
cd "$m"
s=`git describe --always $h`
if echo "$s" | grep -q '^.*-[0-9]\+-g[0-9a-f]\{7\}'
then
s=`echo "$s" |
sed 's/^\(.*\)-\([0-9]\+\)-g\([0-9a-f]\{7\}\)/\1+\2(\3)/'`
else
s="$s(`git rev-parse --short $s^{}`)"
fi
echo $s
}
for m in `git submodule | awk '{print $2}'`
do
commits=`git submodule summary $cached $ref $m | grep '^ ' | wc -l`
test "$commits" -eq 0 &&
continue
hashes=(`git submodule summary $cached --summary-limit 1 $ref $m | head -n1 |
sed -n 's/^\* .* \([0-9a-f]\{7\}\)\.\.\.\([0-9a-f]\{7\}\) ([0-9]\+):$/\1 \2/p'`)
from=`describe $m ${hashes[0]}`
to=`describe $m ${hashes[1]}`
echo "* $m $from...$to ($commits commits)"
limit=
footer=
test "$commits" -gt "$max" && {
limit="--summary-limit $min"
test "$commits" -gt "$min" &&
footer=" (...)\n"
}
git submodule summary $cached $limit $ref $m | sed -e 1d -e \$d
echo -e "$footer"
done
exit 0
deb:
./build-deb
clean:
$(RM) -v *.deb
#!/usr/bin/env python2
# Automatically add a summary of changed modules by using git subsum.
#
# To enable this hook, copy it to $repo/.git/hooks/prepare-commit-msg.
import re
import sys
from subprocess import check_output, CalledProcessError, STDOUT
def die(fmt, *args):
sys.stderr.write((sys.argv[0] + ': ' + fmt + '\n') % args)
sys.exit(1)
def exit(msg, subsum, fname, fmt, *args):
errmsg = (fmt + '\n') % args
sys.stderr.write(sys.argv[0] + ': ' + errmsg)
new_msg = '# Warning (git subsum):\n# \t' + errmsg
if subsum is not None:
new_msg = ("#\n# The new summary is:\n" +
'\n'.join(('# ' + l for l in subsum.splitlines())))
new_msg += msg
try:
file(fname, 'w').write(new_msg)
except Exception as e:
die("Error writing file %s (%s)", fname, e)
sys.exit(0)
# Command line arguments
fname = sys.argv[1]
op = sys.argv[2] if len(sys.argv) > 2 else None
ref = (' %s^' % sys.argv[3]) if len(sys.argv) > 3 else ""
# Read the contents of the file with the message
try:
msg = file(sys.argv[1]).read()
except Exception as e:
die("Error reading file %s (%s)", fname, e)
# Don't do anything if the command is not available
try:
check_output('git -c help.autocorrect=0 subsum -h'.split(),
stderr=STDOUT)
except CalledProcessError as e:
exit(msg, None, fname, "Error running `git subsum`: %s", e)
# Create a temporary file with the contents of the summary
# (and remove it at program exit)
try:
subsum = check_output(('git subsum -c' + ref).split())
except CalledProcessError as e:
exit(msg, fname, "Error invoking `git subsum%s` (%s)", ref, e)
# See githooks(5) man for details on what each value means
# No pre-existing message
if not op:
msg = '\n\n%s%s' % (subsum, msg)
# Pre-existing message
elif op in ('commit', 'message', 'merge'):
# We assume the message is divided in 3 sections:
#
# 1. The user message
# 2. A git subsum summary (optional)
# 3. Comments added by git as a help
#
# We always start copying all the user message as is. When we detect
# the summary started (via a regex), we start commenting out the old
# summary. When the comments added by git are detected (a line starting
# with '#'), then we go back again to copy all the lines as they are.
# Just after we detect the end of the user message (being because the
# summary or the comments started), we print the new subsum summary.
#
# So the resulting new message is:
#
# 1. The user message
# 2. The new git subsum summary (if any)
# 3. The old git subsum summary commented out (if any)
# 4. The comments added by git as a help
IN_MESSAGE = 1
IN_SUMMARY = 2
IN_COMMENT = 3
state = IN_MESSAGE
# Regex to detect the beginning of the old summary information
sum_re = re.compile(r'^\* [^ ]+ .*\([0-9a-f]{7}\)\.\.\.'
r'.*\([0-9a-f]{7}\) \([0-9]+ commits\)$')
new_msg = ''
for line in msg.splitlines():
if state == IN_MESSAGE:
if sum_re.match(line):
state = IN_SUMMARY
new_msg += subsum
# only put comments for interactive message
if op != 'message':
new_msg += '# Old submodules ' + \
'summary:\n#\n# ' + line + '\n'
continue
if line.startswith('#'):
state = IN_COMMENT
new_msg += '\n' + subsum + line + '\n'
continue
new_msg += line + '\n'
continue
if state == IN_SUMMARY:
if line.startswith('#'):
status = IN_COMMENT
new_msg += line + '\n'
continue
# only put comments for interactive message
if op != 'message':
new_msg += '# ' + line + '\n'
continue
if state == IN_COMMENT:
new_msg += line + '\n'
continue
msg = new_msg
# Squashing is ignored for now, as we could have multiple git subsum generated
# messages, the simple message parsing used above is not good enough and
# everything will break. But we let the user know via a comment in the commit
# message.
elif op in ('squash',):
exit(msg, subsum, fname, "Squashing not supported yet, the summary "
"wasn't updated")
# We don't mess with people configuring templates at all.
elif op in ('template',):
sys.exit(0)
# Any unknown operation is only reported to the user
else:
exit(msg, subsum, fname, "Unkown argument '%s' (cmd line: %s)", op,
' '.join(["'%s'" % a for a in sys.argv]))
try:
file(fname, 'w').write(msg)
except Exception as e:
die("Error writing file %s (%s)", fname, e)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment