Skip to content
Browse files

Initial push of the git hooks.

  • Loading branch information...
1 parent 75b98d1 commit 249af6c6a8b328451228dead5679ebb391bd1b9a @zukhan zukhan committed
View
37 hooks/README
@@ -0,0 +1,37 @@
+Copyright (c) 2012 by Delphix.
+All rights reserved.
+
+The Git SCM executes certain 'hooks' for various operations. We customize some
+of these hooks in our Git repository to provide custom checks and to take
+actions such as sending automated emails.
+
+See githooks(5) for information about the semantics of each individual hook.
+
+The gate configuration is stored in the git configuration file. There is the
+default [gate] section optional [gate "base"] sections where top-level sub-
+directory configuration can be stored. This allows different top-level sub-
+directories to behave slightly differently in terms of the checks required and
+the notification lists. The required fields are marked with a *.
+
+[gate]
+ name* The name of the gate (must match the directory name)
+ notify* The email address[es] to which notification is sent
+ shortname An identifier for pushes to the gate
+ approvers The reviewboard approver group required for pushes
+ user-check Set to 'skip' to allow non-git user pushes
+ commit-check Set to 'skip' to allow multiple commits per push
+ comment-check Set to 'skip' to allow free-form (non-bug) comments
+ review-check Set to 'skip' to allow unreviewed pushes
+
+ mail-debug [debug-only] Print email subjects, but don't send
+ bug-update [debug-only] Set to 'skip' to not close bugs on push
+
+[gate "base"]
+ shortname An alternate identifier for this base
+ notify* The email address[es] to which notification is sent
+ user-check Set to 'skip' to allow non-git user pushes
+ commit-check Set to 'skip' to allow multiple commits per push
+ comment-check Set to 'skip' to allow free-form (non-bug) comments
+ review-check Set to 'skip' to allow unreviewed pushes
+
+ bug-update [debug-only] Set to 'skip' to not close bugs on push
View
112 hooks/bugzilla/check-bugs
@@ -0,0 +1,112 @@
+#!/usr/bin/python2.6
+#
+# Copyright (c) 2012 by Delphix.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# - Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# - Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+#
+# This script checks a list of bugs to verify that they are properly formed,
+# that the summaries match in Bugzilla. The list of bugs is provided via stdin.
+#
+# If there's an error, it will emit a message about the changes required.
+#
+# With the -l options, this script will emit the list of bugs, stripping out
+# those that pertain to follow-on fixes.
+#
+
+import xmlrpclib
+import pyzilla
+import sys
+import re
+from getopt import getopt
+
+
+def usage():
+ print "Usage: check-bugs [-l]"
+ sys.exit(2)
+
+(opts, args) = getopt(sys.argv[1:], "l")
+
+if args:
+ usage()
+
+lines = sys.stdin.readlines()
+badlines = []
+
+ids = []
+summaries = []
+for i in range(0, len(lines)):
+ try:
+ line = lines[i].rstrip("\n")
+ (bugid, summary) = line.split(" ", 1)
+ ids.append(int(bugid))
+ summaries.append(summary)
+ except ValueError:
+ badlines.append(line)
+
+server = pyzilla.BugZilla("<bugzilla_url>")
+server.login("<username>", "<password>")
+
+try:
+ bugs = server.Bug.get({"ids": ids})["bugs"]
+except xmlrpclib.Fault as (e):
+ print e
+ sys.exit(1)
+
+goodbugs = []
+error = False
+for i in range(0, len(bugs)):
+ bug = bugs[i]
+ bugid = bug["id"]
+ summary = bug["summary"]
+ regex = " \(.+\)$"
+
+ #
+ # If the summary matches exactly, remember the bug id. If the summary
+ # beings with the correct text and is followed by a comment within
+ # parentheses, we treat this as a follow-on push. Otherwise, identify the
+ # error along with the correct summary.
+ #
+ if summary == summaries[i]:
+ goodbugs.append(bugid)
+ elif not (summaries[i].startswith(summary) and
+ re.match(regex, summaries[i][len(summary):])):
+ if not error:
+ print "Bug summaries must match with data in Bugzilla"
+ error = True
+ print "-", ids[i], summaries[i]
+ print "+", bugid, summary
+
+if len(badlines) != 0:
+ error = True
+ print "commit lines must be of the form <id> <summary>"
+ for i in range(0, len(badlines)):
+ print ">", badlines[i]
+
+if error:
+ sys.exit(1)
+
+if opts:
+ print " ".join(map(str, goodbugs))
+
+sys.exit(0)
View
79 hooks/bugzilla/mark-integrated
@@ -0,0 +1,79 @@
+#!/usr/bin/python2.6
+#
+# Copyright (c) 2011 by Delphix.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# - Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# - Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+#
+# This script marks bugs as integrated and adds a comment to note the gate
+# that they were pushed to.
+#
+
+import xmlrpclib
+import pyzilla
+import sys
+import os
+import re
+
+
+def usage():
+ print "Usage: mark-integrated <bugid> ..."
+ sys.exit(2)
+
+if len(sys.argv) <= 1:
+ usage()
+
+ids = sys.argv[1:]
+
+git_dir = os.getenv("GIT_DIR")
+if not git_dir:
+ print "GIT_DIR is not set"
+ sys.exit(1)
+
+p = re.compile(".*/([^/]+)/.git$")
+m = p.match(git_dir)
+
+if not m:
+ print "GIT_DIR is invalid: " + git_dir
+ sys.exit(1)
+
+gate = m.group(1)
+
+server = pyzilla.BugZilla("<bugzilla_url>")
+server.login("<username>", "<password>")
+
+try:
+ server.Bug.update({
+ "ids": ids,
+ "status": "INTEGRATED",
+ "comment": {
+ "body": "pushed to " + gate
+ }
+ })
+except xmlrpclib.Fault as (e):
+ print e
+ sys.exit(1)
+
+print "bugs marked as integrated to " + gate
+
+sys.exit(0)
View
111 hooks/bugzilla/pyzilla.py
@@ -0,0 +1,111 @@
+#
+# Copyright (c) 2011 by Delphix.
+# All rights reserved.
+#
+
+# pyzilla.py is a Python wrapper for the xmlrpc interface of bugzilla
+# Copyright (C) <2010> <Noufal Ibrahim>
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+#
+# Note: changed SafeTransport to Transport since our Bugzilla is not over https
+#
+
+import os
+import sys
+import xmlrpclib
+import urllib2
+import logging
+import cookielib
+
+def create_user_agent():
+ ma, mi, rel = sys.version_info[:3]
+ return "xmlrpclib - Python-%s.%s.%s"%(ma, mi, rel)
+
+class CookieAuthXMLRPCTransport(xmlrpclib.Transport):
+ """
+ xmlrpclib.Transport that caches authentication cookies in a
+ local cookie jar and reuses them.
+
+ Based off `this recipe
+ <http://code.activestate.com/recipes/501148-xmlrpc-serverclient-which-does-cookie-handling-and/>`_
+
+ """
+
+ def __init__(self, cookiefile = False, user_agent = False):
+ self.cookiefile = cookiefile or "cookies.txt"
+ self.user_agent = user_agent or create_user_agent()
+ xmlrpclib.Transport.__init__(self)
+
+ def send_cookie_auth(self, connection):
+ """Include Cookie Authentication data in a header"""
+ logging.debug("Sending cookie")
+ cj = cookielib.LWPCookieJar()
+ cj.load(self.cookiefile)
+ for cookie in cj:
+ connection.putheader("Cookie", "%s=%s" % (cookie.name,cookie.value))
+
+ ## override the send_host hook to also send authentication info
+ def send_host(self, connection, host):
+ xmlrpclib.Transport.send_host(self, connection, host)
+ if os.path.exists(self.cookiefile):
+ logging.debug(" Sending back cookie header")
+ self.send_cookie_auth(connection)
+
+ def request(self, host, handler, request_body, verbose=0):
+ # dummy request class for extracting cookies
+ class CookieRequest(urllib2.Request):
+ pass
+ # dummy response class for extracting cookies
+ class CookieResponse:
+ def __init__(self, headers):
+ self.headers = headers
+ def info(self):
+ return self.headers
+ crequest = CookieRequest('http://'+host+'/')
+ # issue XML-RPC request
+ h = self.make_connection(host)
+ if verbose:
+ h.set_debuglevel(1)
+ self.send_request(h, handler, request_body)
+ self.send_host(h, host)
+ self.send_user_agent(h)
+ # creating a cookie jar for my cookies
+ cj = cookielib.LWPCookieJar()
+ self.send_content(h, request_body)
+ errcode, errmsg, headers = h.getreply()
+ cresponse = CookieResponse(headers)
+ cj.extract_cookies(cresponse, crequest)
+ if len(cj) >0 and not os.path.exists(self.cookiefile):
+ logging.debug("Saving cookies in cookie jar")
+ cj.save(self.cookiefile)
+ if errcode != 200:
+ raise xmlrpclib.ProtocolError(host + handler,
+ errcode, errmsg,headers)
+ self.verbose = verbose
+ try:
+ sock = h._conn.sock
+ except AttributeError:
+ sock = None
+ return self._parse_response(h.getfile(), sock)
+
+class BugZilla(xmlrpclib.Server):
+ def __init__(self, url, verbose = False):
+ xmlrpclib.Server.__init__(self, url, CookieAuthXMLRPCTransport(),
+ verbose = verbose)
+
+ def login(self, username, password):
+ self.User.login (dict(login=username,
+ password = password))
View
103 hooks/dlpx-common
@@ -0,0 +1,103 @@
+#
+# Copyright (c) 2012 by Delphix.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# - Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# - Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+function fail
+{
+ echo "$(basename $0): $*" >&2
+ exit 1
+}
+
+#
+# The standard input to the pre-receive and post-update hooks is a line for
+# each ref of the following form:
+#
+# <old-value> <new-value> <ref-name>
+#
+# We expect a single such line; these define the index of the changes being
+# pushed.
+#
+function init
+{
+ read line || fail "there must be at least one ref"
+ set -- $line
+ from=$1
+ to=$2
+
+ read && fail "there must be exactly one ref"
+}
+
+#
+# This is a helper routine for the ubiquitous use of 'git log ...' in here.
+#
+function git_log
+{
+ git log $* ${from}..${to}
+}
+
+#
+# Determine if the push consists entirely of commits in the 'exclude' list.
+# This list is used by gatekeepers to allow for syncing release gates or
+# otherwise bypassing the normal checks. To make use of this feature, the list
+# of commit hashes (retrieved by git log --pretty="%H" origin/master..HEAD)
+# must be placed in '.git/bypass' of the target gate. If all commits are
+# covered by this list, then we skip all checks.
+#
+function is_bypassed
+{
+ local commit
+
+ [[ -f $GIT_DIR/bypass ]] || return 1
+
+ for commit in $(git_log --pretty="%H"); do
+ grep $commit $GIT_DIR/bypass >/dev/null 2>&1 || return 1
+ done
+
+ return 0
+}
+
+#
+# Check if the given property is set for any of the top-level directories that
+# contain modifications. The administor can set a default for the entire gate,
+# and also override settings for specific top-level directories. If any
+# relevant values are not set to "skip" then we return true.
+#
+function gate_prop
+{
+ local prop=$1
+ local bases gateprop baseprop
+
+ bases=$(git_log --pretty="format:" --name-only | cut -d/ -f1 | uniq)
+ gateprop=$(git config --get gate.$prop)
+ for base in $bases; do
+ baseprop=$(git config --get gate.$base.$prop)
+ if [[ $? -ne 0 ]]; then
+ [[ "x$gateprop" != "xskip" ]] && return 0
+ else
+ [[ "x$baseprop" != "xskip" ]] && return 0
+ fi
+ done
+
+ return 1
+}
View
112 hooks/dlpx-update-hooks
@@ -0,0 +1,112 @@
+#!/bin/bash
+#
+# Copyright (c) 2011 by Delphix.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# - Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# - Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+#
+# When a user pushes new hooks, the post-update hook invokes this script so
+# that it can update the actual hooks in the git repository. We do this by
+# making a copy of ourselves in a temporary location and executing that so
+# that we can update the hooks safely.
+#
+
+opt_G=false # -G don't exec ourselves as a temporary copy
+opt_D=false # -D delete ourselves after execution
+
+function fail
+{
+ echo "$(basename $0): $*" >&2
+ exit 1
+}
+
+OPTIND=1; while getopts 'DG' c; do
+ case "$c" in
+ D|G) eval opt_$c=true ;;
+ *) fail "illegal option -- $OPTARG" ;;
+ esac
+done
+
+let OPTIND="$OPTIND - 1"; shift $OPTIND
+
+if [[ $opt_D = true ]]; then
+ case "$0" in
+ /tmp/*) ;;
+ *) fail "-D specified, but the executable is not in /tmp"
+ esac
+fi
+
+#
+# We re-exec ourselves in /tmp so that we can safely copy over the previous
+# version of this script.
+#
+if [[ $opt_G = false ]]; then
+ cmd="/tmp/$(basename $0).$$"
+ cp $0 $cmd || fail "copy to /tmp failed"
+ exec $cmd -DG $0
+fi
+
+orig_exec=$1
+dir=
+
+case "$orig_exec" in
+ /*) dir="$(dirname $orig_exec)" ;;
+ *) dir="$(pwd)/$(dirname $orig_exec)" ;;
+esac
+
+cd $dir
+hooks_dir=$(pwd)
+
+[[ $(basename $hooks_dir) = "hooks" ]] || \
+ fail "expected to be in the hooks directory ($hooks_dir)"
+[[ $(basename $(dirname $hooks_dir)) = ".git" ]] || \
+ fail "expected to be in the .git/hooks directory ($hooks_dir)"
+
+newhooks_dir="$(dirname $hooks_dir)/newhooks"
+tmphooks_dir="$(dirname $hooks_dir)/tmphooks"
+
+# Copy the hooks from the repository into a new hooks directory.
+rm -rf $newhooks_dir
+cp -r "${dir}/../../tools/git/hooks" $newhooks_dir || \
+ fail "failed to copy hooks"
+
+# make sure all the hooks have the right permissions
+cd $newhooks_dir
+for hook in *; do
+ case "$hook" in
+ README) ;;
+ *) chmod 755 $hook ;;
+ esac
+done
+
+# Move the new hooks into place and remove the old hooks.
+mv $hooks_dir $tmphooks_dir || fail "couldn't move hooks aside"
+mv $newhooks_dir $hooks_dir || fail "couldn't install new hooks"
+rm -rf $tmphooks_dir
+
+echo "Completed git hooks updated."
+
+# Delete ourselves from /tmp.
+$opt_D && rm $0
+
+exit 0
View
187 hooks/post-receive
@@ -0,0 +1,187 @@
+#!/bin/bash
+#
+# Copyright (c) 2012 by Delphix.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# - Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# - Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+#
+# This hook is invoked by git as a result of a push after all new updates have
+# been received. We use this for any post-push activities that don't directly
+# manipulate files.
+#
+
+if [[ -z "$GIT_DIR" ]]; then
+ echo "GIT_DIR is not set"
+ exit 1
+fi
+GIT_DIR=$(readlink -f $GIT_DIR)
+
+. $GIT_DIR/hooks/dlpx-common
+
+function mail_body
+{
+ local type t
+ local gargs="--pretty=format: --name-only -M90%"
+
+ #
+ # Emit the header whose format differs depending on whether this was a
+ # bypassed push or not.
+ #
+ if is_bypassed; then
+ git log --pretty='%h %an <%ae>%n%B' ${from}..${to}
+ else
+ git_log
+ fi
+
+ for type in added modified renamed deleted other; do
+ case $type in
+ added) t=A ;;
+ modified) t=M ;;
+ renamed) t=R ;;
+ deleted) t=D ;;
+ other) t=CTUXB ;;
+ esac
+
+ #
+ # If there are no entries of this type, skip it.
+ #
+ [[ -z $(git_log $gargs --diff-filter=$t) ]] && continue
+
+ echo
+ echo "$type:"
+ git_log $gargs --diff-filter=$t | sort | uniq | grep -v '^$'
+ done
+}
+
+#
+# Send mail notifying users of the change. The gate administrator can set up
+# special notifications for particular top-level directories if desired.
+#
+function send_mail
+{
+ local author email action bases b got_app
+ local mail_debug=false
+ local target=
+ local comment=
+
+ git config --get gate.mail-debug >/dev/null 2>&1 && mail_debug=true
+
+ if is_bypassed; then
+ author="Delphix Gatekeeper"
+ email="xxx@delphix.com"
+ action="gatekeeper push"
+ else
+ author=$(git_log -1 --pretty="%cn")
+ email=$(git_log -1 --pretty="%ce")
+ action="push"
+
+ #
+ # If comments are vetted, then we'll list the bugs fixed;
+ # otherwise we'll use the first line of the commit message.
+ #
+ if gate_prop comment-check; then
+ b=$(git_log --pretty="%B" -1 | cut -d' ' -f1)
+ comment=" (bugs $(echo $b))"
+ else
+ b=$(git_log --pretty="%B" -1 | head -1)
+ [[ -n "$b" ]] && comment=" ($(echo $b))"
+ fi
+ fi
+
+ do_default=false
+ bases=$(git_log --pretty="format:" --name-only | cut -d/ -f1 | uniq)
+ for base in $bases; do
+
+ basenotify=$(git config --get gate.$base.notify)
+ if [[ $? -ne 0 ]]; then
+ do_default=true
+ continue
+ fi
+
+ baseshortname=$(git config --get gate.shortname)
+ [[ $? -ne 0 ]] && baseshortname=$base
+ target=" to $baseshortname"
+
+ if $mail_debug; then
+ echo "$basenotify [$gate] $action$target$comment"
+ else
+ mail_body | nail \
+ -s "[$gate] $action$target$comment" \
+ -r "$author <${email}>" \
+ "$basenotify"
+ fi
+ done
+
+ if $do_default; then
+
+ gatenotify=$(git config --get gate.notify)
+
+ gateshortname=$(git config --get gate.shortname)
+ [[ $? -eq 0 ]] && target=" to $gateshortname"
+
+ if $mail_debug; then
+ echo "$gatenotify [$gate] $action$target$comment"
+ else
+ mail_body | nail \
+ -s "[$gate] $action $target$comment" \
+ -r "$author <${email}>" \
+ "$gatenotify"
+ fi
+ fi
+}
+
+#
+# Mark any pending reviews in reviewboard associated with these bugs as fixed,
+# and mark the bugs in bugzilla as integrated. See the comments in
+# check_review() of the pre-receive hook for more information on this
+# processing.
+#
+function update_review_and_bugs
+{
+ local bugs
+
+ bugs=$(git_log --pretty="%B" | sed -n '$!p' | \
+ $GIT_DIR/hooks/bugzilla/check-bugs -l)
+
+ gate_prop review-check && \
+ $GIT_DIR/hooks/reviewboard/mark-submitted $bugs
+ gate_prop comment-check && \
+ $GIT_DIR/hooks/bugzilla/mark-integrated $bugs
+}
+
+# do our common initialization.
+init
+
+gate=$(basename $(dirname $GIT_DIR))
+
+send_mail
+
+if ! is_bypassed; then
+ if gate_prop comment-check && gate_prop bug-update; then
+ update_review_and_bugs
+ else
+ echo "skipping bug update"
+ fi
+fi
+
+exit 0
View
74 hooks/post-update
@@ -0,0 +1,74 @@
+#!/bin/bash
+#
+# Copyright (c) 2012 by Delphix.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# - Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# - Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+#
+# This hook is invoked by git as a result of a push after all new updates have
+# been applied. We use this to update files in the gate, and to update hooks
+# as needed.
+#
+
+# close stdout
+exec 1>&-
+exec 2>&-
+
+# log our output
+exec 1>>/var/tmp/out 2>&1
+
+if [[ -z "$GIT_DIR" ]]; then
+ echo "GIT_DIR is not set"
+ exit 1
+fi
+GIT_DIR=$(readlink -f $GIT_DIR)
+
+#
+# Update the files in the repository to reflect the newly pushed changes.
+#
+cd $GIT_DIR/..
+pwd
+git reset --hard
+
+gate=$(basename $(dirname $GIT_DIR))
+
+#
+# Check to see if any of our git hooks have changed, and update them if they
+# have. This only applies to the main delphix-gate -- the hooks for all other
+# gates are symlinks to the delphix-gate hooks.
+#
+[[ "$gate" = delphix-gate ]] || exit 0
+
+hooks_update=false
+while read file; do
+ [[ $(dirname $file) =~ ^tools/git/hooks ]] && hooks_update=true
+done < <(git log --pretty="format:" --name-only -1)
+
+#
+# If one or more git hooks were changed, call our helper script to update them.
+#
+if $hooks_update; then
+ exec "$GIT_DIR/hooks/dlpx-update-hooks"
+fi
+
+exit 0
View
232 hooks/pre-receive
@@ -0,0 +1,232 @@
+#!/bin/bash
+#
+# Copyright (c) 2012 by Delphix.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# - Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# - Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+#
+# This hook is invoked by git as the result of a push before any actions are
+# actually taken.
+#
+
+DOMAIN=delphix.com
+
+if [[ -z "$GIT_DIR" ]]; then
+ echo "GIT_DIR is not set"
+ exit 1
+fi
+GIT_DIR=$(readlink -f $GIT_DIR)
+
+. $GIT_DIR/hooks/dlpx-common
+
+#
+# Make sure there's a single commit message.
+#
+function check_commit
+{
+ local count=$(git_log --pretty="%H" | wc -l)
+ local branch
+
+ if [[ $count -ne 1 ]]; then
+ branch=$(git symbolic-ref HEAD)
+ branch=${branch##refs/heads/}
+ echo
+ echo "Multiple commits per push are not permitted."
+ echo "Use the following command to reset deltas:"
+ echo " git reset --soft origin/$branch"
+ echo " git commit"
+
+ exit 1
+ fi
+}
+
+function check_comment_failure
+{
+ echo
+ echo "Use the following command to edit the last commit message:"
+ echo " git commit --amend --reset-author"
+ echo
+
+ exit 1
+}
+
+#
+# Make sure that comments are properly formatted.
+#
+function check_comment
+{
+ #
+ # Check that the author is using a legitimate email address.
+ #
+ local email=$(git_log --pretty="%ce")
+ if [[ $? -ne 0 ]]; then
+ echo "git log failed: line $LINENO" >&2
+ exit 1
+ fi
+
+ if [[ ${email#*@} != $DOMAIN ]]; then
+ echo
+ echo "The address $email is invalid; use a $DOMAIN address."
+ echo
+ echo "To fix this, use the following commands:"
+ echo " git config [--global] user.name '<first> <last>'"
+ echo " git config [--global] user.email '<user>@$DOMAIN'"
+ echo "(use --global to set the global defaults)"
+
+ check_comment_failure
+ fi
+
+ #
+ # Check that the comments contain no blank lines save for a single
+ # one at the end.
+ #
+ blank=false
+ git_log --pretty="%B" | while read line; do
+ # If there's a blank in the middle, exit the while sub-shell.
+ $blank && exit 1
+ [[ -z $line ]] && blank=true || blank=false
+ done
+
+ if [[ $? -ne 0 ]]; then
+ echo
+ echo "The commit comments contain spurious newlines"
+
+ git_log --pretty="%B" | \
+ sed -n -e 's/./ &/' -e 's/^$/> /' -e '$!p'
+
+ check_comment_failure
+ fi
+
+ #
+ # Check that the bugs all conform to the proper form and that each
+ # summary matches the information in Bugzilla.
+ #
+ git_log --pretty="%B" | sed -n '$!p' | \
+ $GIT_DIR/hooks/bugzilla/check-bugs || check_comment_failure
+}
+
+#
+# Make sure that all bugs are covered by a pending review, and that the state
+# of the review meets acceptable critera (two reviewers, at least one a
+# gatekeeper, and all reviewers have approved the request). We explicitly
+# ignore bugs that appear to be follow-on bugs of the form:
+#
+# <id> <synopsis> (...)
+#
+# Such as:
+#
+# 1000 address some issue (missed file)
+# 1000 address some issue (fix build)
+#
+# Because we assume that the review will have already been marked as submitted
+# and therefore would otherwise be inappropriately rejected. We use the
+# Bugzilla check-bugs script to produce a list of bugs ids to check.
+#
+function check_review
+{
+ local bugs approvers
+
+ bugs=$(git_log --pretty="%B" | sed -n '$!p' | \
+ $GIT_DIR/hooks/bugzilla/check-bugs -l)
+
+ # This is a strange failure since we just checked...
+ [[ $? -eq 0 ]] || exit 1
+
+ [[ -z $bugs ]] && return
+
+ #
+ # Check who the appropriate approvers are for this gate, defaulting to
+ # gatekeepers if the property is not set.
+ #
+ approvers=$(git config --get gate.approvers)
+ [[ $? -ne 0 || -z $approvers ]] && approvers=gatekeepers
+
+ $GIT_DIR/hooks/reviewboard/check-bugs $bugs || exit 1
+ $GIT_DIR/hooks/reviewboard/check-approvers $approvers $bugs || exit 1
+
+ git_log --pretty="format:" --name-only | grep -v '^$' | \
+ $GIT_DIR/hooks/reviewboard/check-changed-files $bugs || exit 1
+}
+
+#
+# Make sure that checkins are done as the 'git' user.
+#
+function check_user
+{
+ if [[ $(whoami) != "git" ]]; then
+ echo "Must be the 'git' user to push"
+ exit 1
+ fi
+}
+
+# do our common initialization.
+init
+
+gate=$(basename $(dirname $GIT_DIR))
+
+gatename=$(git config --get gate.name)
+if [[ $? -ne 0 ]]; then
+ echo
+ echo "This git repo has not been set up as a gate."
+ echo "Contact the administrator or gatekeeper."
+ echo
+ exit 1
+fi
+
+if [[ "x$gatename" != "x$gate" ]]; then
+ echo
+ echo "The gate configuration lists $gatename rather than $gate."
+ echo "Contact the administrator or gatekeeper."
+ echo
+ exit 1
+fi
+
+gatenotify=$(git config --get gate.notify)
+if [[ $? -ne 0 || -z $gatenotify ]]; then
+ echo
+ echo "The gate configuration is missing a notification address."
+ echo "Contact the administrator or gatekeeper."
+ echo
+ exit 1
+fi
+
+
+#
+# Make sure we're executing as the git user unless the gate.user property is
+# set to 'skip'.
+#
+[[ "x$(git config --get gate.user-check)" != "xskip" ]] && check_user
+
+if ! is_bypassed; then
+ gate_prop commit-check && check_commit
+ gate_prop comment-check && check_comment
+ gate_prop review-check && check_review
+fi
+
+git_log --pretty="format:" --name-only | grep -v '^$' | while read file; do
+ echo "changing file: $file"
+done
+
+echo "Checks successful."
+
+exit 0
View
121 hooks/reviewboard/check-approvers
@@ -0,0 +1,121 @@
+#!/usr/bin/python2.6
+#
+# Copyright (c) 2012 by Delphix.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# - Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# - Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+#
+# This script verifies that each review associated with a set of bugs is
+# well-formed. This comprises the following checks:
+#
+# 1. There are at least two reviews marked "ship it".
+#
+# 2. At least one of the approvers is in the specified reviewboard group.
+#
+# 3. For each user that has commented on a review, make sure that the last
+# comment is marked "ship it" -- the exception being reviews from the
+# submitter which are ignored.
+#
+# Examples:
+#
+# check-approvers gatekeepers 9378 10013
+# check-approvers qa-gatekeepers 10234
+#
+# This script assumes that the bug list has successfully passed 'check-bugs'
+# and therefore all bugs are covered by a pending review. If any pending
+# review doesn't match the above criteria, an error is printed and a non-zero
+# exit status is returned.
+#
+
+import sys
+import rblib
+import rbutils
+
+
+def check_review(requestid):
+ ret = 0
+ request = server.get_review_request(requestid)
+ submitter = request['links']['submitter']['title']
+
+ # Iterate over all reviews and remember the last review by each user.
+ reviews = server.api_get_list(request['links']['reviews']['href'],
+ 'reviews')
+ reviewer = {}
+ for r in reviews:
+ try:
+ #
+ # This will fail if the given user has been disabled. We can
+ # safely catch and ignore these failures, considering only active
+ # accounts for reviewers and approvers.
+ #
+ user = server.api_get(r['links']['user']['href'])['user']
+ username = user['username']
+ if username != submitter:
+ reviewer[username] = r
+ except rblib.APIError:
+ pass
+
+ approved = False
+ for u in reviewer:
+ r = reviewer[u]
+ if not r['ship_it']:
+ print "ERROR: review %d must be approved by '%s'" % \
+ (request['id'], u)
+ ret = 1
+ elif u in approvers:
+ approved = True
+
+ if not approved:
+ print "ERROR: review %d has not been " % (request['id']) + \
+ "approved by a gatekeeper"
+ ret = 1
+
+ if len(reviewer) < 2:
+ print "ERROR: review %d must have at least 2 reviewers" % \
+ (request['id'])
+ ret = 1
+
+ return ret
+
+# Get the list of reviewboard approvers
+server = rblib.ReviewBoardServer()
+approvers = {}
+try:
+ group = server.get_group(sys.argv[1])
+ members = server.api_get_list(group['links']['users']['href'], 'users')
+ for u in members:
+ approvers[u['username']] = True
+except rblib.APIError:
+ pass
+
+if not approvers:
+ print "ERROR: the approvers list is empty"
+ sys.exit(1)
+
+toprocess = rbutils.get_reviews(sys.argv[2:])
+exitstatus = 0
+for rid in toprocess.keys():
+ if check_review(rid) != 0:
+ exitstatus = 1
+
+sys.exit(exitstatus)
View
68 hooks/reviewboard/check-bugs
@@ -0,0 +1,68 @@
+#!/usr/bin/python2.6
+#
+# Copyright (c) 2011 by Delphix.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# - Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# - Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+#
+# This script verifies that a set of bug IDs correspond to bugs that are part
+# of a pending review request. To use it, run:
+#
+# check-bugs <bugid> ...
+#
+# For example:
+#
+# check-bugs 9378 10013
+#
+# The script will check all reviews in the 'pending' state and process the
+# "bugs" field. If there are any bugs specified in the arguments that are not
+# accounted for by pending reviews, an error message is printed and a non-zero
+# exit status is returned.
+#
+# Note that this doesn't check that the set of bugs exactly matches a complete
+# set of a single review. This means that it will allow bugs in the review
+# that are not addressed by comments, as well as allowing a single push to be
+# covered by multiple reviews. This is generally poor form (the set of diffs
+# reviewed as a unit should be pushed as a unit), but there are times when it
+# is useful to break up a set of changes into smaller reviews.
+#
+
+import rblib
+import sys
+
+seen = {}
+
+server = rblib.ReviewBoardServer()
+reviews = server.get_pending_reviews()
+for r in reviews:
+ for bugid in r['bugs_closed']:
+ seen[bugid] = r['id']
+
+exitstatus = 0
+for i in xrange(1, len(sys.argv)):
+ bugid = sys.argv[i]
+ if not bugid in seen:
+ print "ERROR: bug %s is not covered by a pending review" % (bugid)
+ exitstatus = 1
+
+sys.exit(exitstatus)
View
108 hooks/reviewboard/check-changed-files
@@ -0,0 +1,108 @@
+#!/usr/bin/python2.6
+#
+# Copyright (c) 2012 by Delphix.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# - Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# - Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+#
+# This script verifies that for all reviews associated with a set of bugs,
+# the list of changed files in those reviews matches the list of files that
+# are included in the git commit to be pushed.
+#
+
+import sys
+import os
+import rblib
+import rbutils
+
+
+def compare_changed_files():
+ """
+ Verifies that the list of changed files in the review request matches
+ the list of changed files in the git commit.
+ """
+ git_files_list = sys.stdin.readlines()
+ git_files_list = [line.strip() for line in git_files_list]
+
+ # Sort the two lists
+ git_files_list.sort()
+ rb_files_list.sort()
+
+ # Check that the list of changed files match
+ if git_files_list == rb_files_list:
+ return 0
+
+ # Get the diff between the two lists
+ rb_diff = set(rb_files_list).difference(set(git_files_list))
+ git_diff = set(git_files_list).difference(set(rb_files_list))
+
+ print
+ print "ERROR: the list of files being pushed does not match the contents"
+ print "of the review request."
+ print
+
+ if len(rb_diff) > 0:
+ print "The following files are not included in your push:"
+ for rb_file in rb_diff:
+ print ' ' + rb_file
+ print
+
+ if len(git_diff) > 0:
+ print "The following files are not covered by a code review:"
+ for git_file in git_diff:
+ print ' ' + git_file
+ print
+
+ return 1
+
+
+def remember_changed_files(rid):
+ """
+ For each review, we want to remember the list of changed files
+ in the latest diff of that review.
+ """
+ server = rblib.ReviewBoardServer()
+ request = server.get_review_request(rid)
+
+ get_diffs_url = request['links']['diffs']['href']
+ diffs_resource = server.api_get(get_diffs_url)
+ diffs_list = server.api_get_list(get_diffs_url, 'diffs')
+
+ total_diffs = diffs_resource['total_results']
+ files_url = diffs_list[total_diffs - 1]['links']['files']['href']
+ files_list = server.api_get_list(files_url, 'files')
+
+ files = [f['dest_file'] for f in files_list]
+ rb_files_list.extend(files)
+
+toprocess = rbutils.get_reviews(sys.argv[1:])
+
+rb_files_list = []
+for id in toprocess.keys():
+ remember_changed_files(id)
+
+exitstatus = 0
+if compare_changed_files() != 0:
+ exitstatus = 1
+
+sys.exit(exitstatus)
View
59 hooks/reviewboard/mark-submitted
@@ -0,0 +1,59 @@
+#!/usr/bin/env python
+#
+# Copyright (c) 2011 by Delphix.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# - Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# - Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+#
+# Given a set of bug IDs, this script will mark any reviews as submitted that
+# address the given set of bugs. To use it, run:
+#
+# mark-submitted <bugid> [bugid ...]
+#
+# For example:
+#
+# mark-submitted 9378 10013
+#
+# The script will check all reviews in the 'pending' state and process the
+# "bugs" field. If there are any bugs specified in the arguments that are
+# part of a review, that review will be marked submitted.
+#
+
+import rblib
+import sys
+
+bugs = {}
+for i in xrange(1, len(sys.argv)):
+ bugid = sys.argv[i]
+ bugs[bugid] = True
+
+server = rblib.ReviewBoardServer()
+reviews = server.get_pending_reviews()
+for r in reviews:
+ for bugid in r['bugs_closed']:
+ if bugid in bugs:
+ print "marking review %s submitted" % (r['id'])
+ server.mark_submitted(r)
+ break
+
+sys.exit(0)
View
633 hooks/reviewboard/rblib.py
@@ -0,0 +1,633 @@
+#
+# Copyright (c) 2011 by Delphix.
+# All rights reserved.
+#
+
+#
+# Delphix ReviewBoard integration library. This module is based on the RBTools
+# package, and is designed solely to work with the Delphix ReviewBoard
+# installation in the context of executing git hooks.
+#
+# It automatically handles logging into the server as the 'git' user, which is a
+# normal user with the additional privilege of being able to change the status
+# of other user's reviews.
+#
+# To use the library, simply do the following:
+#
+# server = rblib.ReviewBoardServer()
+#
+# The server currently supports the following methods:
+#
+# get_pending_review_requests([olderthan])
+#
+# Return a list of all reviews in the 'pending' state.
+#
+# get_review_request(id)
+#
+# Get a particular review request.
+#
+# mark_submitted(request)
+#
+# Mark a particular review request as submitted.
+#
+
+#
+# Copyright (c) 2007-2010 Christian Hammond
+# Copyright (c) 2007-2010 David Trowbridge
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy of
+# this software and associated documentation files (the "Software"), to deal in
+# the Software without restriction, including without limitation the rights to
+# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+# of the Software, and to permit persons to whom the Software is furnished to do
+# so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+
+###
+# Configuration Options
+###
+
+REVIEWBOARD_URL = "<rb_url>"
+REVIEWBOARD_USER = "<username>"
+REVIEWBOARD_PASSWORD = "<password>"
+
+###
+# End of Configuration Options
+###
+
+import base64
+import cookielib
+import difflib
+import getpass
+import marshal
+import mimetools
+import ntpath
+import os
+import re
+import socket
+import stat
+import subprocess
+import sys
+import tempfile
+import datetime
+import urllib
+import urllib2
+from optparse import OptionParser
+from pkg_resources import parse_version
+from tempfile import mkstemp
+from urlparse import urljoin, urlparse
+
+try:
+ from hashlib import md5
+except ImportError:
+ # Support Python versions before 2.5.
+ from md5 import md5
+
+try:
+ # Specifically import json_loads, to work around some issues with
+ # installations containing incompatible modules named "json".
+ from json import loads as json_loads
+except ImportError:
+ from simplejson import loads as json_loads
+
+import posixpath as cpath
+
+class APIError(Exception):
+ def __init__(self, http_status, error_code, rsp=None, *args, **kwargs):
+ Exception.__init__(self, *args, **kwargs)
+ self.http_status = http_status
+ self.error_code = error_code
+ self.rsp = rsp
+
+ def __str__(self):
+ code_str = "HTTP %d" % self.http_status
+
+ if self.error_code:
+ code_str += ', API Error %d' % self.error_code
+
+ if self.rsp and 'err' in self.rsp:
+ return '%s (%s)' % (self.rsp['err']['msg'], code_str)
+ else:
+ return code_str
+
+
+class HTTPRequest(urllib2.Request):
+ def __init__(self, url, body, headers={}, method="PUT"):
+ urllib2.Request.__init__(self, url, body, headers)
+ self.method = method
+
+ def get_method(self):
+ return self.method
+
+
+class PresetHTTPAuthHandler(urllib2.BaseHandler):
+ """urllib2 handler that conditionally presets the use of HTTP Basic Auth.
+
+ This is used when specifying --username= on the command line. It will
+ force an HTTP_AUTHORIZATION header with the user info, asking the user
+ for any missing info beforehand. It will then try this header for that
+ first request.
+
+ It will only do this once.
+ """
+ handler_order = 480 # After Basic auth
+
+ def __init__(self, url, password_mgr):
+ self.url = url
+ self.password_mgr = password_mgr
+ self.used = False
+
+ def reset(self):
+ self.password_mgr.rb_user = None
+ self.password_mgr.rb_pass = None
+ self.used = False
+
+ def http_request(self, request):
+ if REVIEWBOARD_USER and not self.used:
+ # Note that we call password_mgr.find_user_password to get the
+ # username and password we're working with. This allows us to
+ # prompt if, say, --username was specified but --password was not.
+ username, password = \
+ self.password_mgr.find_user_password('Web API', self.url)
+ raw = '%s:%s' % (username, password)
+ request.add_header(
+ urllib2.HTTPBasicAuthHandler.auth_header,
+ 'Basic %s' % base64.b64encode(raw).strip())
+ self.used = True
+
+ return request
+
+ https_request = http_request
+
+
+class ReviewBoardHTTPErrorProcessor(urllib2.HTTPErrorProcessor):
+ """Processes HTTP error codes.
+
+ Python 2.6 gets HTTP error code processing right, but 2.4 and 2.5 only
+ accepts HTTP 200 and 206 as success codes. This handler ensures that
+ anything in the 200 range is a success.
+ """
+ def http_response(self, request, response):
+ if not (200 <= response.code < 300):
+ response = self.parent.error('http', request, response,
+ response.code, response.msg,
+ response.info())
+
+ return response
+
+ https_response = http_response
+
+
+class ReviewBoardHTTPBasicAuthHandler(urllib2.HTTPBasicAuthHandler):
+ """Custom Basic Auth handler that doesn't retry excessively.
+
+ urllib2's HTTPBasicAuthHandler retries over and over, which is useless.
+ This subclass only retries once to make sure we've attempted with a
+ valid username and password. It will then fail so we can use
+ tempt_fate's retry handler.
+ """
+ def __init__(self, *args, **kwargs):
+ urllib2.HTTPBasicAuthHandler.__init__(self, *args, **kwargs)
+ self._retried = False
+
+ def retry_http_basic_auth(self, *args, **kwargs):
+ if not self._retried:
+ self._retried = True
+ response = urllib2.HTTPBasicAuthHandler.retry_http_basic_auth(
+ self, *args, **kwargs)
+
+ if response.code != 401:
+ self._retried = False
+
+ return response
+ else:
+ return None
+
+
+class ReviewBoardHTTPPasswordMgr(urllib2.HTTPPasswordMgr):
+ """
+ Adds HTTP authentication support for URLs.
+
+ Python 2.4's password manager has a bug in http authentication when the
+ target server uses a non-standard port. This works around that bug on
+ Python 2.4 installs. This also allows post-review to prompt for passwords
+ in a consistent way.
+
+ See: http://bugs.python.org/issue974757
+ """
+ def __init__(self, reviewboard_url, rb_user=None, rb_pass=None):
+ self.passwd = {}
+ self.rb_url = reviewboard_url
+ self.rb_user = rb_user
+ self.rb_pass = rb_pass
+
+ def find_user_password(self, realm, uri):
+ if uri.startswith(self.rb_url):
+ if self.rb_user is None or self.rb_pass is None:
+
+ print "==> HTTP Authentication Required"
+ print 'Enter authorization information for "%s" at %s' % \
+ (realm, urlparse(uri)[1])
+
+ if not self.rb_user:
+ self.rb_user = raw_input('Username: ')
+
+ if not self.rb_pass:
+ self.rb_pass = getpass.getpass('Password: ')
+
+ return self.rb_user, self.rb_pass
+ else:
+ # If this is an auth request for some other domain (since HTTP
+ # handlers are global), fall back to standard password management.
+ return urllib2.HTTPPasswordMgr.find_user_password(self, realm, uri)
+
+
+class ReviewBoardServer(object):
+ """
+ An instance of a Review Board server.
+ """
+ def __init__(self):
+ self.url = REVIEWBOARD_URL
+ if self.url[-1] != '/':
+ self.url += '/'
+ self._server_info = None
+ self.root_resource = None
+
+ if 'HOME' in os.environ:
+ homepath = os.environ["HOME"]
+ else:
+ homepath = ''
+
+ # Load the config and cookie files
+ self.cookie_file = os.path.join(homepath, ".post-review-cookies.txt")
+ self.cookie_jar = cookielib.MozillaCookieJar(self.cookie_file)
+
+ if self.cookie_file:
+ try:
+ self.cookie_jar.load(self.cookie_file, ignore_expires=True)
+ except IOError:
+ pass
+
+ # Set up the HTTP libraries to support all of the features we need.
+ cookie_handler = urllib2.HTTPCookieProcessor(self.cookie_jar)
+ password_mgr = ReviewBoardHTTPPasswordMgr(self.url, REVIEWBOARD_USER,
+ REVIEWBOARD_PASSWORD)
+ basic_auth_handler = ReviewBoardHTTPBasicAuthHandler(password_mgr)
+ digest_auth_handler = urllib2.HTTPDigestAuthHandler(password_mgr)
+ self.preset_auth_handler = PresetHTTPAuthHandler(self.url, password_mgr)
+ http_error_processor = ReviewBoardHTTPErrorProcessor()
+
+ opener = urllib2.build_opener(cookie_handler, basic_auth_handler,
+ digest_auth_handler, self.preset_auth_handler, http_error_processor)
+ opener.addheaders = [('User-agent', 'rblib')]
+ urllib2.install_opener(opener)
+
+ self.login()
+
+ def login(self, force=False):
+ """
+ Logs in to a Review Board server, prompting the user for login
+ information if needed.
+ """
+
+ self.root_resource = self.api_get('api/')
+ rsp = self.api_get(self.root_resource['links']['info']['href'])
+
+ self.rb_version = rsp['info']['product']['package_version']
+
+ if force:
+ self.preset_auth_handler.reset()
+
+ def has_valid_cookie(self):
+ """
+ Load the user's cookie file and see if they have a valid
+ 'rbsessionid' cookie for the current Review Board server. Returns
+ true if so and false otherwise.
+ """
+ try:
+ parsed_url = urlparse(self.url)
+ host = parsed_url[1]
+ path = parsed_url[2] or '/'
+
+ # Cookie files don't store port numbers, unfortunately, so
+ # get rid of the port number if it's present.
+ host = host.split(":")[0]
+
+ # Cookie files also append .local to bare hostnames
+ if '.' not in host:
+ host += '.local'
+
+ try:
+ cookie = self.cookie_jar._cookies[host][path]['rbsessionid']
+
+ if not cookie.is_expired():
+ return True
+
+ except KeyError:
+ pass
+
+ except IOError, error:
+ pass
+
+ return False
+
+ def _get_server_info(self):
+ if not self._server_info:
+ self._server_info = self._info.find_server_repository_info(self)
+
+ return self._server_info
+
+ info = property(_get_server_info)
+
+ def process_json(self, data):
+ """
+ Loads in a JSON file and returns the data if successful. On failure,
+ APIError is raised.
+ """
+ rsp = json_loads(data)
+
+ if rsp['stat'] == 'fail':
+ # With the new API, we should get something other than HTTP
+ # 200 for errors, in which case we wouldn't get this far.
+ self.process_error(200, data)
+
+ return rsp
+
+ def process_error(self, http_status, data):
+ """Processes an error, raising an APIError with the information."""
+ try:
+ rsp = json_loads(data)
+
+ assert rsp['stat'] == 'fail'
+
+ raise APIError(http_status, rsp['err']['code'], rsp,
+ rsp['err']['msg'])
+ except ValueError:
+ raise APIError(http_status, None, None, data)
+
+ def http_get(self, path):
+ """
+ Performs an HTTP GET on the specified path, storing any cookies that
+ were set.
+ """
+
+ url = self._make_url(path)
+ rsp = urllib2.urlopen(url).read()
+
+ try:
+ self.cookie_jar.save(self.cookie_file)
+ except IOError, e:
+ pass
+ return rsp
+
+ def _make_url(self, path):
+ """Given a path on the server returns a full http:// style url"""
+ if path.startswith('http'):
+ # This is already a full path.
+ return path
+
+ app = urlparse(self.url)[2]
+
+ if path[0] == '/':
+ url = urljoin(self.url, app[:-1] + path)
+ else:
+ url = urljoin(self.url, app + path)
+
+ if not url.startswith('http'):
+ url = 'http://%s' % url
+ return url
+
+ def api_get(self, path, args=None):
+ """
+ Performs an API call using HTTP GET at the specified path.
+ """
+ if args:
+ first = True
+ for name in args:
+ if first:
+ path += "?"
+ first = False
+ else:
+ path += "&"
+ path += "%s=%s"%(name, args[name])
+ try:
+ return self.process_json(self.http_get(path))
+ except urllib2.HTTPError, e:
+ self.process_error(e.code, e.read())
+
+ def http_post(self, path, fields, files=None):
+ """
+ Performs an HTTP POST on the specified path, storing any cookies that
+ were set.
+ """
+ if fields:
+ debug_fields = fields.copy()
+ else:
+ debug_fields = {}
+
+ if 'password' in debug_fields:
+ debug_fields["password"] = "**************"
+ url = self._make_url(path)
+
+ content_type, body = self._encode_multipart_formdata(fields, files)
+ headers = {
+ 'Content-Type': content_type,
+ 'Content-Length': str(len(body))
+ }
+
+ try:
+ r = urllib2.Request(url, body, headers)
+ data = urllib2.urlopen(r).read()
+ try:
+ self.cookie_jar.save(self.cookie_file)
+ except IOError, e:
+ pass
+ return data
+ except urllib2.HTTPError, e:
+ # Re-raise so callers can interpret it.
+ raise e
+ except urllib2.URLError, e:
+ try:
+ e.read()
+ except AttributeError:
+ pass
+
+ die("Unable to access %s. The host path may be invalid\n%s" % \
+ (url, e))
+
+ def http_put(self, path, fields):
+ """
+ Performs an HTTP PUT on the specified path, storing any cookies that
+ were set.
+ """
+ url = self._make_url(path)
+
+ content_type, body = self._encode_multipart_formdata(fields, None)
+ headers = {
+ 'Content-Type': content_type,
+ 'Content-Length': str(len(body))
+ }
+
+ try:
+ r = HTTPRequest(url, body, headers, method='PUT')
+ data = urllib2.urlopen(r).read()
+ self.cookie_jar.save(self.cookie_file)
+ return data
+ except urllib2.HTTPError, e:
+ # Re-raise so callers can interpret it.
+ raise e
+ except urllib2.URLError, e:
+ try:
+ e.read()
+ except AttributeError:
+ pass
+
+ die("Unable to access %s. The host path may be invalid\n%s" % \
+ (url, e))
+
+ def http_delete(self, path):
+ """
+ Performs an HTTP DELETE on the specified path, storing any cookies that
+ were set.
+ """
+ url = self._make_url(path)
+
+ try:
+ r = HTTPRequest(url, body, headers, method='DELETE')
+ data = urllib2.urlopen(r).read()
+ self.cookie_jar.save(self.cookie_file)
+ return data
+ except urllib2.HTTPError, e:
+ # Re-raise so callers can interpret it.
+ raise e
+ except urllib2.URLError, e:
+ try:
+ e.read()
+ except AttributeError:
+ pass
+
+ die("Unable to access %s. The host path may be invalid\n%s" % \
+ (url, e))
+
+ def api_post(self, path, fields=None, files=None):
+ """
+ Performs an API call using HTTP POST at the specified path.
+ """
+ try:
+ return self.process_json(self.http_post(path, fields, files))
+ except urllib2.HTTPError, e:
+ self.process_error(e.code, e.read())
+
+ def api_put(self, path, fields=None):
+ """
+ Performs an API call using HTTP PUT at the specified path.
+ """
+ try:
+ return self.process_json(self.http_put(path, fields))
+ except urllib2.HTTPError, e:
+ self.process_error(e.code, e.read())
+
+ def api_delete(self, path):
+ """
+ Performs an API call using HTTP DELETE at the specified path.
+ """
+ try:
+ return self.process_json(self.http_delete(path))
+ except urllib2.HTTPError, e:
+ self.process_error(e.code, e.read())
+
+ def _encode_multipart_formdata(self, fields, files):
+ """
+ Encodes data for use in an HTTP POST.
+ """
+ BOUNDARY = mimetools.choose_boundary()
+ content = ""
+
+ fields = fields or {}
+ files = files or {}
+
+ for key in fields:
+ content += "--" + BOUNDARY + "\r\n"
+ content += "Content-Disposition: form-data; name=\"%s\"\r\n" % key
+ content += "\r\n"
+ content += str(fields[key]) + "\r\n"
+
+ for key in files:
+ filename = files[key]['filename']
+ value = files[key]['content']
+ content += "--" + BOUNDARY + "\r\n"
+ content += "Content-Disposition: form-data; name=\"%s\"; " % key
+ content += "filename=\"%s\"\r\n" % filename
+ content += "\r\n"
+ content += value + "\r\n"
+
+ content += "--" + BOUNDARY + "--\r\n"
+ content += "\r\n"
+
+ content_type = "multipart/form-data; boundary=%s" % BOUNDARY
+
+ return content_type, content
+
+ def get_review_request(self, rid):
+ """
+ Returns the review request with the specified ID.
+ """
+ url = '%s%s/' % (
+ self.root_resource['links']['review_requests']['href'], rid)
+
+ rsp = self.api_get(url)
+
+ return rsp['review_request']
+
+ def get_pending_reviews(self, older_than=None):
+ """
+ Return a list of all reviews in the pending state
+ """
+ params = {
+ 'status': 'pending',
+ 'max-results': 50
+ }
+
+ if older_than:
+ params['last-updated-to'] = str(older_than)
+
+ return self.api_get_list(
+ self.root_resource['links']['review_requests']['href'],
+ 'review_requests', params)
+
+ def api_get_list(self, link, name, params={ 'max-results': 50 }):
+ result = self.api_get(link, params)
+ ret = result[name]
+ offset = 0
+ pagesize = params['max-results']
+ while offset + pagesize < result['total_results']:
+ offset += pagesize
+ params['start'] = offset
+ result = self.api_get(link, params)
+ ret += result[name]
+
+ return ret
+
+ def get_group(self, groupname):
+ """
+ Returns the group with the given name.
+ """
+ url = '%s%s/' % (
+ self.root_resource['links']['groups']['href'], groupname)
+
+ return self.api_get(url)['group']
+
+ def mark_submitted(self, review_request):
+ self.api_put(review_request['links']['self']['href'], {
+ 'status': 'submitted',
+ })
+
+
+def die(msg=None):
+ if msg:
+ print msg
+
+ sys.exit(1)
View
45 hooks/reviewboard/rbutils.py
@@ -0,0 +1,45 @@
+#!/usr/bin/python2.6
+#
+# Copyright (c) 2012 by Delphix.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# - Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# - Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+#
+# This script provides the utility method for getting a list of reviews from
+# a set of bugs.
+#
+
+import rblib
+
+
+def get_reviews(bugs):
+ toprocess = {}
+
+ server = rblib.ReviewBoardServer()
+ reviews = server.get_pending_reviews()
+ for r in reviews:
+ for id in r['bugs_closed']:
+ for bug in bugs:
+ if bug == id:
+ toprocess[r['id']] = True
+ return toprocess

0 comments on commit 249af6c

Please sign in to comment.
Something went wrong with that request. Please try again.