blob: 0ed6131bfa42230717e3bbb48dda91391dd02159 [file] [log] [blame]
#!/bin/bash
#
# Verifies whether Brillo kernel patches are well-formed w.r.t. the guidelines
# in device/generic/brillo/KernelDevelopmentGuide.md
#
# The checks are certainly not comprehensive, but should cover the basic
# requirements.
#
# There is some additional checking implemented that allows to verify whether a
# change that's an annotated amalgamation of some history looks good, i.e.
# the resulting changes are the same, signoff information got retained etc.
# Globals to track the number of commits and number of defects.
ncommits=0
ndefects=0
# Print an error message and exit.
abort() {
echo -e "$@" 1>&2
exit -1
}
# Prints a diagnostic and increments the defects count. This should be called
# once for each check that fails.
diag() {
let "ndefects++"
[[ -n "${QUIET}" ]] || echo -e " $@" 1>&2
}
# Checks whether a given git revision is valid.
is_valid_rev() {
git rev-parse -q --verify "$1" 1>/dev/null
}
# Verifies whether ${commit} title is prefixed as stated by the rules.
check_subject_prefix() {
git log -1 --pretty=%s "${commit}" \
| grep -q \
-e '^UPSTREAM:' \
-e '^FROMLIST:' \
-e '^BACKPORT:' \
-e '^RFC:' \
-e '^ANDROID:' \
-e '^BRILLO:' \
-e '^CHROMIUM:' \
-e '^VENDOR: [A-Za-z0-9]\+:' \
|| diag "Missing or bad commit subject prefix"
}
# Verifies that there's a Bug: line in ${commit}.
check_bug_info() {
git log -1 --pretty=%B "${commit}" \
| grep -qi '^Bug[:=]\s*[A-Za-z0-9_:-]\+\(\s\|$\)' \
|| diag "Missing bug annotation"
}
# Verifies that ${commit} declares the patchset name in a Patchset: annotation
# and that it matches the current branch. If there's no current branch, skips
# the check (this is useful to check a detached master branch as checked out by
# repo).
check_patchset() {
local patchset="$(
git log -1 --pretty=%B "${commit}" \
| grep '^Patchset:\s*[A-Za-z0-9_-]\+\(\s\|$\)' \
| awk '{ print $2 }')"
if [[ -z "${patchset}" ]]; then
diag "Missing patchset annotation"
return
fi
local branch="$(git rev-parse --abbrev-ref "$HEAD_REV")"
if [[ -n "${branch}" ]]; then
[[ "${branch}" == "${patchset}" ]] \
|| diag "Patchset name ${patchset} doesn't match branch name ${branch}"
fi
}
# Verifies that ${commit} has a sign-off line for the author.
check_signed_off_by_author() {
local committer="$(git log -1 --pretty='%cn <%ce>' "${commit}")"
git log -1 --pretty=%B "${commit}" \
| grep -q "^Signed-off-by:\s*${committer}\(\s\|$\)" \
|| diag "Signed-off-by line for committer is missing"
}
# Verifies that ${commit} was correctly constructed from reference commits
# appearing in ${REF_HEAD_REV}.
check_commit_provenance() {
[[ -n "${REF_HEAD_REV}" ]] || return
local ref_commits=
for ref_commit in $(git log --reverse --pretty=%h \
"${BASE_REV}..${REF_HEAD_REV}"); do
# See whether ${ref_commit} got rolled into ${commit}. We check whether the
# commit title appears. If it does, assume the commit got included.
local ref_title="$(git log -1 --pretty=%s "${ref_commit}")"
if git log -1 --pretty=%B "${commit}" | grep -q "${ref_title}"; then
ref_commits+="${ref_commit} "
continue
fi
done
if [[ -z "${ref_commits}" ]]; then
diag "Failed to find reference commit"
return
fi
# Check that all signoff lines are retained, output per-commit status if
# signoff information got dropped.
local signoff="$(
git log -1 --pretty=%B "${commit}" | grep "^Signed-off-by:" | sort -u)"
local ref_signoff="$(echo "${ref_commits}" \
| xargs -n 1 git log -1 --pretty=%B | grep "^Signed-off-by:" | sort -u)"
local signoff_diff="$(
comm -2 -3 <(echo "${ref_signoff}") <(echo "${signoff}"))"
if [[ -n "${signoff_diff}" ]]; then
local signoff_diag="Signed-off-by lines lost:\\n"
signoff_diag+="$(echo "${signoff_diag}" | awk '{ print " " $0 }')"
signoff_diag+="\\n Suggested sign-off lines:\\n"
sigonff_diag+="$(echo "$ref_commits" | xargs -n 1 git log -1 --pretty=%B \
| grep "^Signed-off-by:" | awk '!x[$0]++' | awk '{ print " " $0 }')"
diag "${signoff_diag}"
fi
}
usage() {
cat 1>&2 <<END
Usage: $(basename $0) <options> [<commit>]
Checks commits from the branch head <commit> (or HEAD if not specified) for
compliance with Brillo kernel commit guidelines.
Options include:
-b <commit> Specify the base commit to start checking commits from. If this
is not specified, use the head <commit>'s upstream.
-h Print usage information and exit.
-q Quiet mode. Do not print diagnostics, but set exit status.
-r <commit> A commit to use as a reference to validate checked commits
against. Makes sure that the resulting diff between the common
base and <head> vs. the diff between base and reference are
identical and checks that signoff information got preserved.
END
exit -2
}
# Parses command line options and stores them in constants.
parse_options() {
local progname="$(basename $0)"
while [[ $# -gt 0 ]]; do
case "$1" in
-b)
BASE_REV="$2"
shift
;;
-h)
usage
;;
-q)
QUIET=1
;;
-r)
REF_HEAD_REV="$2"
shift
;;
-*)
abort "Unknown option $1"
;;
*)
break
;;
esac
shift
done
# Process remaining args, if any.
HEAD_REV="${1:-HEAD}"
is_valid_rev "${HEAD_REV}" || abort "${HEAD_REV} is not a valid git rev!"
readonly HEAD_REV
if [[ -z "${BASE_REV}" ]]; then
BASE_REV="$(git rev-parse --abbrev-ref --symbolic-full-name \
"${HEAD_REV}@{u}")"
fi
is_valid_rev "${BASE_REV}" || abort "${BASE_REV} is not a valid git rev!"
readonly BASE_REV
if [[ -n "${REF_HEAD_REV}" ]]; then
is_valid_rev "${REF_HEAD_REV}" \
|| abort "{$REF_HEAD_REV} is not a valid git rev!"
fi
readonly REF_HEAD_REV
readonly QUIET
}
main() {
parse_options "$@"
# Go through all commits from base to head revision.
for commit in $(git log --reverse --pretty=%h "${BASE_REV}..${HEAD_REV}"); do
let "ncommits++"
[[ -n "${QUIET}" ]] || git log -1 --oneline "${commit}" 1>&2
# Check the commit for basic adherence to the rules.
check_subject_prefix
check_bug_info
check_patchset
check_signed_off_by_author
# Verify that the commit was correctly derived from reference commit(s).
check_commit_provenance
done
# Check that the final tree matches the reference tree.
if [[ -n "${REF_HEAD_REV}" ]]; then
local tree_diff=$(git diff -w "${REF_HEAD_REV}..${HEAD_REV}")
if [[ -n "$tree_diff" ]]; then
diag "Reference tree differs:"
# diag() escapes its arguments and may thus mangle the patch, so emit it
# directly to standard error instead.
[[ -n "${QUIET}" ]] || echo "${tree_diff}" \
| awk '{ print " " $0 }' 1>&2
fi
fi
# Print results.
if [[ -z "${QUIET}" ]]; then
if [[ "${ndefects}" -eq 0 ]]; then
echo "${ncommits} commits meet basic Brillo kernel style, congrats!"
else
echo "${ndefects} defects detected in ${ncommits} commits."
fi 1>&2
fi
exit "${ndefects}"
}
main "$@"