diff options
author | Michał Górny <mgorny@gentoo.org> | 2018-09-27 21:16:10 +0200 |
---|---|---|
committer | Michał Górny <mgorny@gentoo.org> | 2019-04-09 13:05:55 +0200 |
commit | 7ff69af5ba056831c4373af3a5b8be6595f8dee2 (patch) | |
tree | 21bbf28e1e8cf1ce6db4bdb82861fd52596655da | |
parent | update-02-gpg: Try to fix locale in glep63-check output (diff) | |
download | githooks-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.sh | 72 | ||||
-rwxr-xr-x | local/tests/update-06-copyright.sh | 314 | ||||
-rwxr-xr-x | local/update-06-copyright | 138 |
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}" |