aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichał Górny <mgorny@gentoo.org>2018-09-27 21:16:10 +0200
committerMichał Górny <mgorny@gentoo.org>2019-04-09 13:05:55 +0200
commit7ff69af5ba056831c4373af3a5b8be6595f8dee2 (patch)
tree21bbf28e1e8cf1ce6db4bdb82861fd52596655da
parentupdate-02-gpg: Try to fix locale in glep63-check output (diff)
downloadgithooks-7ff69af5ba056831c4373af3a5b8be6595f8dee2.tar.gz
githooks-7ff69af5ba056831c4373af3a5b8be6595f8dee2.tar.bz2
githooks-7ff69af5ba056831c4373af3a5b8be6595f8dee2.zip
Hook to test GCO sign-off
Signed-off-by: Michał Górny <mgorny@gentoo.org>
-rw-r--r--local/tests/lib.sh72
-rwxr-xr-xlocal/tests/update-06-copyright.sh314
-rwxr-xr-xlocal/update-06-copyright138
3 files changed, 524 insertions, 0 deletions
diff --git a/local/tests/lib.sh b/local/tests/lib.sh
new file mode 100644
index 0000000..8ecefdf
--- /dev/null
+++ b/local/tests/lib.sh
@@ -0,0 +1,72 @@
+#!/bin/bash
+# Git hook test helpers
+# Copyright 2018 Michał Górny
+# Distributed under the terms of the GNU General Public License v2 or later
+
+. /lib/gentoo/functions.sh
+
+die() {
+ echo "died @ ${BASH_SOURCE[1]}:${BASH_LINENO[0]}" >&2
+ exit 1
+}
+
+# Starts a test. Creates temporary git repo and enters it.
+# $1 - test description (printed)
+tbegin() {
+ local desc=${1}
+ TEST_DIR=$(mktemp -d) || die
+ export GIT_DIR=${TEST_DIR}/.git
+
+ pushd "${TEST_DIR}" >/dev/null || die
+ git init -q || die
+ # create an initial commit to avoid a lot of pain ;-)
+ git commit -q --allow-empty -m 'empty initial commit' || die
+
+ ebegin "${desc}"
+}
+
+# Finish a test. Does popd and cleans up the temporary directory.
+# $1 - test result (defaults to $?)
+# $2 - error message (optional)
+tend() {
+ local ret=${1:-${?}}
+ local msg=${2}
+
+ popd >/dev/null || die
+ rm -rf "${TEST_DIR}" || die
+
+ eend "${ret}" "${msg}"
+}
+
+run_test() {
+ local initial_commit
+ initial_commit=$(git rev-list --all | tail -n 1) || die
+ (
+ set -- refs/heads/master "${initial_commit}" HEAD
+ set +e
+ . "${HOOK_PATH}"
+ )
+}
+
+# Run the hook for all commits since the initial commit.
+# Expect success.
+test_success() {
+ run_test
+ tend ${?}
+}
+
+# Run the hook for all commits since the initial commit.
+# Expect failure with message matching the pattern.
+# $1 - bash pattern to match
+test_failure() {
+ local expected=${1}
+ local msg
+
+ if msg=$(run_test); then
+ tend 1 "Hook unexpectedly succeeded"
+ return 1
+ fi
+
+ [[ ${msg} == ${expected} ]]
+ tend ${?} "'${msg}' != '${expected}'"
+}
diff --git a/local/tests/update-06-copyright.sh b/local/tests/update-06-copyright.sh
new file mode 100755
index 0000000..7710c98
--- /dev/null
+++ b/local/tests/update-06-copyright.sh
@@ -0,0 +1,314 @@
+#!/bin/bash
+# Tests for update-06-copyright hook
+# Copyright 2018 Michał Górny
+# Distributed under the terms of the GNU General Public License v2 or later
+
+. "${BASH_SOURCE%/*}"/lib.sh
+HOOK_PATH=${BASH_SOURCE%/*}/../update-06-copyright
+[[ ${HOOK_PATH} == /* ]] || HOOK_PATH=${PWD}/${HOOK_PATH}
+
+# Override ldapsearch for the purpose of the tests
+ldapsearch() {
+ case ${GL_USER} in
+ fakedev@gentoo.org)
+ cat <<-EOF
+ dn: uid=fakedev,ou=devs,dc=gentoo,dc=org
+ cn: I. M. Developer
+ gecos: I. M. Developer
+ EOF
+ ;;
+ polisher@gentoo.org)
+ cat <<-EOF
+ dn: uid=polisher,ou=devs,dc=gentoo,dc=org
+ cn:: xITEh8SZxYLFhMOzxZvFusW8IFBvbGlzaGVyCg==
+ gecos: Acelnoszz Polisher
+ EOF
+ ;;
+ otherdev@gentoo.org)
+ cat <<-EOF
+ dn: uid=otherdev,ou=devs,dc=gentoo,dc=org
+ cn: John O. Developer
+ gecos: John O. Developer
+ EOF
+ ;;
+ esac
+}
+
+# Error message patterns
+FAIL_NO_SIGNOFF="*: no GCO sign-off present"
+FAIL_EMAIL="*: no sign-off matching committer's e-mail address found!
+*"
+FAIL_SYNTAX="*: malformed sign-off (should be: real name <email>)!
+*"
+FAIL_REALNAME="*: name of sign-off does not match realname in LDAP!
+*"
+
+# Non-developer commit tests (for repos that allow those)
+export GL_USER=nondev@example.com
+export GIT_COMMITTER_NAME='Non A. Dev'
+export GIT_COMMITTER_EMAIL=${GL_USER}
+export GIT_AUTHOR_NAME=${GIT_COMMITTER_NAME}
+export GIT_AUTHOR_EMAIL=${GIT_COMMITTER_EMAIL}
+
+einfo "Simple non-developer commit tests"
+eindent
+
+tbegin "Valid GCO sign-off"
+git commit --allow-empty -m "A commit
+
+Signed-off-by: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>" -q
+test_success
+
+tbegin "Multiple sign-offs (including valid)"
+git commit --allow-empty -m "A commit
+
+Signed-off-by: Somebody Else <else@example.com>
+Some-other-tag: blah blah
+Signed-off-by: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>
+Signed-off-by: Also Him <him@example.com>" -q
+test_success
+
+tbegin "No sign-off"
+git commit --allow-empty -m "A commit" -q
+test_failure "${FAIL_NO_SIGNOFF}"
+
+tbegin "Mismatched e-mail address in sign-off"
+git commit --allow-empty -m "A commit
+
+Signed-off-by: ${GIT_COMMITTER_NAME} <foo@example.com>" -q
+test_failure "${FAIL_EMAIL}"
+
+tbegin "Different name in sign-off"
+git commit --allow-empty -m "A commit
+
+Signed-off-by: Foo Bar <${GIT_COMMITTER_EMAIL}>" -q
+test_success
+
+tbegin "Case-insensitive e-mail matching"
+git commit --allow-empty -m "A commit
+
+Signed-off-by: ${GIT_COMMITTER_NAME^^} <${GIT_COMMITTER_EMAIL^^}>" -q
+test_success
+
+tbegin "Linux DCO sign-off"
+git commit --allow-empty -m "A commit
+
+Signed-off-by: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}> (DCO-1.1)" -q
+test_success
+
+eoutdent
+
+einfo "Syntax check tests"
+eindent
+
+tbegin "Invalid sign-off (no e-mail address)"
+git commit --allow-empty -m "A commit
+
+Signed-off-by: ${GIT_COMMITTER_NAME}" -q
+test_failure "${FAIL_SYNTAX}"
+
+tbegin "Invalid sign-off (no name)"
+git commit --allow-empty -m "A commit
+
+Signed-off-by: <${GIT_COMMITTER_EMAIL}>" -q
+test_failure "${FAIL_SYNTAX}"
+
+tbegin "Invalid + valid sign-off (should be rejected)"
+git commit --allow-empty -m "A commit
+
+Signed-off-by: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>
+Signed-off-by: Yo Momma" -q
+test_failure "${FAIL_SYNTAX}"
+
+eoutdent
+
+# Now with a different author
+export GIT_AUTHOR_NAME='Somebody Else'
+export GIT_AUTHOR_EMAIL='selse@example.com'
+
+einfo "Committer != author, non-developer tests"
+eindent
+
+tbegin "Committer != author (non-dev) with valid GCO sign-off"
+git commit --allow-empty -m "A commit
+
+Signed-off-by: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>" -q
+test_success
+
+tbegin "Committer != author (non-dev) with only author sign-off"
+git commit --allow-empty -m "A commit
+
+Signed-off-by: ${GIT_AUTHOR_NAME} <${GIT_AUTHOR_EMAIL}>" -q
+test_failure "${FAIL_EMAIL}"
+
+eoutdent
+
+# Developer commit tests
+export GL_USER=fakedev@gentoo.org
+export GIT_COMMITTER_NAME='I. M. Developer'
+export GIT_COMMITTER_EMAIL=${GL_USER}
+export GIT_AUTHOR_NAME=${GIT_COMMITTER_NAME}
+export GIT_AUTHOR_EMAIL=${GIT_COMMITTER_EMAIL}
+
+einfo "Simple developer commit tests"
+eindent
+
+tbegin "Valid GCO sign-off"
+git commit --allow-empty -m "A commit
+
+Signed-off-by: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>" -q
+test_success
+
+tbegin "E-mail mismatch"
+git commit --allow-empty -m "A commit
+
+Signed-off-by: ${GIT_COMMITTER_NAME} <fakedev@example.com>" -q
+test_failure "${FAIL_EMAIL}"
+
+tbegin "Real name mismatch"
+git commit --allow-empty -m "A commit
+
+Signed-off-by: fakedev <${GIT_COMMITTER_EMAIL}>" -q
+test_failure "${FAIL_REALNAME}"
+
+tbegin "Case insensitivity"
+git commit --allow-empty -m "A commit
+
+Signed-off-by: ${GIT_COMMITTER_NAME^^} <${GIT_COMMITTER_EMAIL^^}>" -q
+test_success
+
+eoutdent
+
+export GIT_AUTHOR_NAME='Somebody Else'
+export GIT_AUTHOR_EMAIL='selse@example.com'
+
+einfo "Proxied commit tests"
+eindent
+
+tbegin "Developer + author GCO sign-off"
+git commit --allow-empty -m "A commit
+
+Signed-off-by: ${GIT_AUTHOR_NAME} <${GIT_AUTHOR_EMAIL}>
+Signed-off-by: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>" -q
+test_success
+
+tbegin "Only developer GCO sign-off"
+git commit --allow-empty -m "A commit
+
+Signed-off-by: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>" -q
+test_success
+
+tbegin "Only author GCO sign-off"
+git commit --allow-empty -m "A commit
+
+Signed-off-by: ${GIT_AUTHOR_NAME} <${GIT_AUTHOR_EMAIL}>" -q
+test_failure "${FAIL_EMAIL}"
+
+eoutdent
+
+einfo "Hardcore unicode tests"
+eindent
+
+export GL_USER=polisher@gentoo.org
+export GIT_COMMITTER_NAME='Ąćęłńóśźż Polisher'
+export GIT_COMMITTER_EMAIL=${GL_USER}
+export GIT_AUTHOR_NAME=${GIT_COMMITTER_NAME}
+export GIT_AUTHOR_EMAIL=${GIT_COMMITTER_EMAIL}
+
+tbegin "All uppercase"
+git commit --allow-empty -m "A commit
+
+Signed-off-by: ĄĆĘŁŃÓŚŹŻ POLISHER <${GIT_COMMITTER_EMAIL}>" -q
+test_success
+
+tbegin "All lowercase"
+git commit --allow-empty -m "A commit
+
+Signed-off-by: ąćęłńóśźż polisher <${GIT_COMMITTER_EMAIL}>" -q
+test_success
+
+tbegin "Mixed case"
+git commit --allow-empty -m "A commit
+
+Signed-off-by: ąĆĘŁń󜏯 Polisher <${GIT_COMMITTER_EMAIL}>" -q
+test_success
+
+tbegin "ASCII (GECOS) version"
+git commit --allow-empty -m "A commit
+
+Signed-off-by: Acelnoszz Polisher <${GIT_COMMITTER_EMAIL}>" -q
+test_success
+
+tbegin "Uppercase GECOS"
+git commit --allow-empty -m "A commit
+
+Signed-off-by: ACELNOSZZ POLISHER <${GIT_COMMITTER_EMAIL}>" -q
+test_success
+
+tbegin "Lowercase GECOS"
+git commit --allow-empty -m "A commit
+
+Signed-off-by: acelnoszz polisher <${GIT_COMMITTER_EMAIL}>" -q
+test_success
+
+eoutdent
+
+export GL_USER=otherdev@gentoo.org
+export GIT_COMMITTER_NAME='I. M. Developer'
+export GIT_COMMITTER_EMAIL=fakedev@gentoo.org
+export GIT_AUTHOR_NAME=${GIT_COMMITTER_NAME}
+export GIT_AUTHOR_EMAIL=${GIT_COMMITTER_EMAIL}
+
+einfo "Committer != pusher tests"
+eindent
+
+tbegin "Valid GCO sign-off"
+git commit --allow-empty -m "A commit
+
+Signed-off-by: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>" -q
+test_success
+
+tbegin "Pusher GCO sign-off only"
+git commit --allow-empty -m "A commit
+
+Signed-off-by: John O. Developer <${GL_USER}>" -q
+test_failure "${FAIL_EMAIL}"
+
+tbegin "Committer realname mismatch (XFAIL)"
+git commit --allow-empty -m "A commit
+
+Signed-off-by: Wrong Name Here <${GIT_COMMITTER_EMAIL}>" -q
+test_success
+
+eoutdent
+
+einfo "Merge commit tests"
+eindent
+
+tbegin "Merge commit with B-branch commit missing sign-off"
+git checkout -q -b test-branch
+git commit --allow-empty -m "A commit" -q
+git checkout -q master
+export GIT_COMMITTER_NAME='John O. Developer'
+export GIT_COMMITTER_EMAIL=${GL_USER}
+git commit --allow-empty -m "Another master commit
+
+Signed-off-by: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>" -q
+git merge -q -m "Test merge commit
+
+Signed-off-by: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>" -q test-branch
+test_failure "${FAIL_NO_SIGNOFF}"
+
+tbegin "Merge commit missing sign-off"
+git checkout -q -b test-branch
+git commit --allow-empty -m "A commit
+
+Signed-off-by: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>" -q
+git checkout -q master
+git commit --allow-empty -m "Another master commit
+
+Signed-off-by: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>" -q
+git merge -q -m "Test merge commit" -q test-branch
+test_failure "${FAIL_NO_SIGNOFF}"
+
+eoutdent
diff --git a/local/update-06-copyright b/local/update-06-copyright
new file mode 100755
index 0000000..e08ff4e
--- /dev/null
+++ b/local/update-06-copyright
@@ -0,0 +1,138 @@
+#!/bin/bash
+# Verify that GCO sign-off is present in commit messages
+# Copyright 2018 Michał Górny
+# Distributed under the terms of the GNU General Public License v2 or later
+
+# Disable filename expansion
+set -f
+# Force UTF-8
+export LC_CTYPE=en_US.UTF-8
+# Make == case-insensitive
+shopt -s nocasematch
+
+# --- Command line
+refname=${1}
+oldrev=${2}
+newrev=${3}
+
+# --- Safety check
+if [[ -z ${GIT_DIR} ]]; then
+ echo "Don't run this script from the command line." >&2
+ echo " (if you want, you could supply GIT_DIR then run" >&2
+ echo " ${0} <ref> <oldrev> <newrev>)" >&2
+ exit 1
+fi
+
+if [[ -z ${refname} || -z ${oldrev} || -z ${newrev} ]]; then
+ echo "usage: ${0} <ref> <oldrev> <newrev>" >&2
+ exit 1
+fi
+
+# Gentoo devs get extra realname checks
+get_from_ldif() {
+ local key=${1}
+ local line
+
+ while read -r line; do
+ case ${line} in
+ "${key}:: "*)
+ # base64-encoded value
+ base64 -d <<<"${line#*:: }"
+ break
+ ;;
+ "${key}: "*)
+ echo "${line#*: }"
+ break
+ ;;
+ esac
+ done
+}
+
+if [[ ${GL_USER} == *@gentoo.org ]]; then
+ ldif=$(ldapsearch "uid=${GL_USER%@gentoo.org}" -D '' -Z -LLL \
+ cn gecos -o ldif-wrap=no)
+ cn_expected=$(get_from_ldif cn <<<"${ldif}")
+ gecos_expected=$(get_from_ldif gecos <<<"${ldif}")
+
+ if [[ -z ${cn_expected} ]]; then
+ echo "Unable to get cn for ${GL_USER}, please report!" >&2
+ exit 1
+ fi
+ if [[ -z ${gecos_expected} ]]; then
+ echo "Unable to get gecos for ${GL_USER}, please report!" >&2
+ exit 1
+ fi
+fi
+
+ret=0
+
+while read -r commithash; do
+ # verify that the commit message contains Signed-off-by
+ signoff=no
+ committer=$(git show -q --pretty=format:'%ce' "${commithash}")
+
+ while read -r line; do
+ if [[ ${line} == signed-off-by:* ]]; then
+ # verify syntax first. should be:
+ # Signed-off-by: Real Name <email@address> [(maybe something)]
+ if [[ ${line} != 'signed-off-by: '*' <'*@*.*'>'* ]]; then
+ signoff=syntaxerr
+ break
+ fi
+
+ # if we already found the correct one, just verify syntax
+ # of the rest
+ [[ ${signoff} == ok ]] && continue
+
+ # strip the key
+ line=${line#*: }
+
+ mail=${line#*<}
+ mail=${mail%%>*}
+ # different mail? try other signoffs, maybe reject.
+ if [[ ${mail} != "${committer}" ]]; then
+ signoff=diffmail
+ continue
+ fi
+
+ # is it the dev? verify real name then.
+ if [[ ${GL_USER} == *@gentoo.org && ${GL_USER} == "${committer}" ]]; then
+ realname=${line%% <*}
+
+ # require either CN or GECOS to match (to allow
+ # for ASCII spelling)
+ if [[ ${realname} != "${cn_expected}" \
+ && ${realname} != "${gecos_expected}" ]]
+ then
+ signoff=diffname
+ break
+ fi
+ fi
+
+ signoff=ok
+ fi
+ done < <(git show -q --pretty=format:'%b' "${commithash}")
+
+ case ${signoff} in
+ no)
+ echo "${commithash}: no GCO sign-off present"
+ ret=1;;
+ syntaxerr)
+ echo "${commithash}: malformed sign-off (should be: real name <email>)!"
+ echo " ${line}"
+ ret=1;;
+ diffmail)
+ echo "${commithash}: no sign-off matching committer's e-mail address found!"
+ echo " expected: ${committer}"
+ echo " last found: ${mail}"
+ ret=1;;
+ diffname)
+ echo "${commithash}: name of sign-off does not match realname in LDAP!"
+ echo " expected: ${cn_expected} (${gecos_expected})"
+ echo " last found: ${realname}"
+ ret=1;;
+ esac
+done < <(git rev-list "${oldrev}..${newrev}")
+
+# --- Finished
+exit "${ret}"