Add a script to verify commit message guidelines.

This adds a script that verifies whether commits meet the guidelines
for commits to the common Brillo kernel tree. This is far from
comprehensive, but proved to be useful to validate cleanup work.

Change-Id: I864fa61e18baa744d3b66b720ce0936673bd36de
diff --git a/check_brillo_kernel_patch.sh b/check_brillo_kernel_patch.sh
new file mode 100644
index 0000000..0ed6131
--- /dev/null
+++ b/check_brillo_kernel_patch.sh
@@ -0,0 +1,240 @@
+#!/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 "$@"