#!/usr/bin/env python2.6 # Copyright (c) 2010, Code Aurora Forum. 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. # # Neither the name of Code Aurora Forum, Inc. nor the names of its # contributors may be used to endorse or promote products derived # from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 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 is designed to detect when a patchset uploaded to Gerrit is # 'identical' (determined via git-patch-id) and reapply reviews onto the new # patchset from the previous patchset. # Get usage and help info by running: ./trivial_rebase.py --help # Documentation is available here: https://www.codeaurora.org/xwiki/bin/QAEP/Gerrit import json from optparse import OptionParser import subprocess from sys import exit class CheckCallError(OSError): """CheckCall() returned non-0.""" def __init__(self, command, cwd, retcode, stdout, stderr=None): OSError.__init__(self, command, cwd, retcode, stdout, stderr) self.command = command self.cwd = cwd self.retcode = retcode self.stdout = stdout self.stderr = stderr def CheckCall(command, cwd=None): """Like subprocess.check_call() but returns stdout. Works on python 2.4 """ try: process = subprocess.Popen(command, cwd=cwd, stdout=subprocess.PIPE) std_out, std_err = process.communicate() except OSError, e: raise CheckCallError(command, cwd, e.errno, None) if process.returncode: raise CheckCallError(command, cwd, process.returncode, std_out, std_err) return std_out, std_err def GsqlQuery(sql_query, server, port): """Runs a gerrit gsql query and returns the result""" gsql_cmd = ['ssh', '-p', port, server, 'gerrit', 'gsql', '--format', 'JSON', '-c', sql_query] try: (gsql_out, gsql_stderr) = CheckCall(gsql_cmd) except CheckCallError, e: print "return code is %s" % e.retcode print "stdout and stderr is\n%s%s" % (e.stdout, e.stderr) raise new_out = gsql_out.replace('}}\n', '}}\nsplit here\n') return new_out.split('split here\n') def FindPrevRev(changeId, patchset, server, port): """Finds the revision of the previous patch set on the change""" sql_query = ("\"SELECT revision FROM patch_sets,changes WHERE " "patch_sets.change_id = changes.change_id AND " "patch_sets.patch_set_id = %s AND " "changes.change_key = \'%s\'\"" % ((patchset - 1), changeId)) revisions = GsqlQuery(sql_query, server, port) json_dict = json.loads(revisions[0], strict=False) return json_dict["columns"]["revision"] def GetApprovals(changeId, patchset, server, port): """Get all the approvals on a specific patch set Returns a list of approval dicts""" sql_query = ("\"SELECT value,account_id,category_id FROM patch_set_approvals " "WHERE change_id = (SELECT change_id FROM changes WHERE " "patch_set_id = %s AND change_key = \'%s\') AND value <> 0\"" % ((patchset - 1), changeId)) gsql_out = GsqlQuery(sql_query, server, port) approvals = [] for json_str in gsql_out: dict = json.loads(json_str, strict=False) if dict["type"] == "row": approvals.append(dict["columns"]) return approvals def GetEmailFromAcctId(account_id, server, port): """Returns the preferred email address associated with the account_id""" sql_query = ("\"SELECT preferred_email FROM accounts WHERE account_id = %s\"" % account_id) email_addr = GsqlQuery(sql_query, server, port) json_dict = json.loads(email_addr[0], strict=False) return json_dict["columns"]["preferred_email"] def GetPatchId(revision): git_show_cmd = ['git', 'show', revision] patch_id_cmd = ['git', 'patch-id'] patch_id_process = subprocess.Popen(patch_id_cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE) git_show_process = subprocess.Popen(git_show_cmd, stdout=subprocess.PIPE) return patch_id_process.communicate(git_show_process.communicate()[0])[0] def SuExec(server, port, private_key, as_user, cmd): suexec_cmd = ['ssh', '-l', "Gerrit Code Review", '-p', port, server, '-i', private_key, 'suexec', '--as', as_user, '--', cmd] CheckCall(suexec_cmd) def DiffCommitMessages(commit1, commit2): log_cmd1 = ['git', 'log', '--pretty=format:"%an %ae%n%s%n%b"', commit1 + '^!'] commit1_log = CheckCall(log_cmd1) log_cmd2 = ['git', 'log', '--pretty=format:"%an %ae%n%s%n%b"', commit2 + '^!'] commit2_log = CheckCall(log_cmd2) if commit1_log != commit2_log: return True return False def Main(): server = 'localhost' usage = "usage: %prog [--server-port=PORT]" parser = OptionParser(usage=usage) parser.add_option("--change", dest="changeId", help="Change identifier") parser.add_option("--project", help="Project path in Gerrit") parser.add_option("--commit", help="Git commit-ish for this patchset") parser.add_option("--patchset", type="int", help="The patchset number") parser.add_option("--private-key-path", dest="private_key_path", help="Full path to Gerrit SSH daemon's private host key") parser.add_option("--server-port", dest="port", default='29418', help="Port to connect to Gerrit's SSH daemon " "[default: %default]") (options, args) = parser.parse_args() if not options.changeId: parser.print_help() exit(0) if options.patchset == 1: # Nothing to detect on first patchset exit(0) prev_revision = None prev_revision = FindPrevRev(options.changeId, options.patchset, server, options.port) if not prev_revision: # Couldn't find a previous revision exit(0) prev_patch_id = GetPatchId(prev_revision) cur_patch_id = GetPatchId(options.commit) if cur_patch_id.split()[0] != prev_patch_id.split()[0]: # patch-ids don't match exit(0) # Patch ids match. This is a trivial rebase. # In addition to patch-id we should check if the commit message changed. Most # approvers would want to re-review changes when the commit message changes. changed = DiffCommitMessages(prev_revision, options.commit) if changed: # Insert a comment into the change letting the approvers know only the # commit message changed comment_msg = ("\'--message=New patchset patch-id matches previous patchset" ", but commit message has changed.'") comment_cmd = ['ssh', '-p', options.port, server, 'gerrit', 'approve', '--project', options.project, comment_msg, options.commit] CheckCall(comment_cmd) exit(0) # Need to get all approvals on prior patch set, then suexec them onto # this patchset. approvals = GetApprovals(options.changeId, options.patchset, server, options.port) gerrit_approve_msg = ("\'Automatically re-added by Gerrit trivial rebase " "detection script.\'") for approval in approvals: # Note: Sites with different 'copy_min_score' values in the # approval_categories DB table might want different behavior here. # Additional categories should also be added if desired. if approval["category_id"] == "CRVW": approve_category = '--code-review' elif approval["category_id"] == "VRIF": # Don't re-add verifies #approve_category = '--verified' continue elif approval["category_id"] == "SUBM": # We don't care about previous submit attempts continue else: print "Unsupported category: %s" % approval exit(0) score = approval["value"] gerrit_approve_cmd = ['gerrit', 'approve', '--project', options.project, '--message', gerrit_approve_msg, approve_category, score, options.commit] email_addr = GetEmailFromAcctId(approval["account_id"], server, options.port) SuExec(server, options.port, options.private_key_path, email_addr, ' '.join(gerrit_approve_cmd)) exit(0) if __name__ == "__main__": Main()