| #!/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 patch_set_id = %s AND change_id = (SELECT change_id FROM " |
| "changes WHERE 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 <required options> [--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() |