Added trivial_rebase script.
diff --git a/gerritx/cmd/trivial_rebase.py b/gerritx/cmd/trivial_rebase.py
new file mode 100644
index 0000000..19006f9
--- /dev/null
+++ b/gerritx/cmd/trivial_rebase.py
@@ -0,0 +1,262 @@
+#!/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
+import subprocess
+from sys import exit
+
+from optparse import OptionParser as _realOptionParser, AmbiguousOptionError, \
+  BadOptionError
+class OptionParser(_realOptionParser):
+  """Make OptionParser silently swallow unrecognized options."""
+  def _process_args(self, largs, rargs, values):
+    while rargs:
+      try:
+        _realOptionParser._process_args(self, largs, rargs, values)
+      except (AmbiguousOptionError, BadOptionError), e:
+        largs.append(e.opt_str)
+
+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 Gssh(options, api_command):
+  """Makes a Gerrit API call via SSH and returns the stdout results."""
+  ssh_cmd = ['ssh',
+             '-l', 'Gerrit Code Review',
+             '-p', options.port,
+             '-i', options.private_key_path,
+             options.server,
+             api_command]
+  try:
+    return CheckCall(ssh_cmd)[0]
+  except CheckCallError, e:
+    import sys
+    err_template = "call: %s\nreturn code: %s\nstdout: %s\nstderr: %s\n"
+    sys.stderr.write(err_template%(ssh_cmd, e.retcode, e.stdout, e.stderr))
+    raise
+
+def GsqlQuery(sql_query, options):
+  """Runs a gerrit gsql query and returns the result"""
+  gsql_cmd = "gerrit gsql --format JSON -c %s"%sql_query
+  gsql_out = Gssh(options, gsql_cmd)
+  new_out = gsql_out.replace('}}\n', '}}\nsplit here\n')
+  return new_out.split('split here\n')
+
+def FindPrevRev(options):
+  """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\'\"" % ((options.patchset - 1),
+                                                  options.changeId))
+  revisions = GsqlQuery(sql_query, options)
+
+  json_dict = json.loads(revisions[0], strict=False)
+  return json_dict["columns"]["revision"]
+
+def GetApprovals(options):
+  """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\""
+               % ((options.patchset - 1), options.changeId))
+  gsql_out = GsqlQuery(sql_query, options)
+  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 GetPatchId(revision, consider_whitespace=False):
+  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)
+  if consider_whitespace:
+    # This matches on change lines in the patch (those starting with "+"
+    # or "-" but not followed by another of the same), then replaces any
+    # space or tab characters with "%" before calculating a patch-id.
+    replace_ws_cmd = ['sed', r'/^\(+[^+]\|-[^-]\)/y/ \t/%%/']
+    replace_ws_process = subprocess.Popen(replace_ws_cmd,
+                                          stdout=subprocess.PIPE,
+                                          stdin=subprocess.PIPE)
+    return patch_id_process.communicate(
+        replace_ws_process.communicate(git_show_process.communicate()[0])[0]
+        )[0]
+  else:
+    return patch_id_process.communicate(git_show_process.communicate()[0])[0]
+
+def SuExec(options, as_user, cmd):
+  suexec_cmd = "suexec --as %s -- %s"%(as_user, cmd)
+  Gssh(options, 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():
+  usage = "usage: %prog <required options> [optional options]"
+  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("--role-user", dest="role_user",
+                    help="E-mail/ID of user commenting on commit messages")
+  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]")
+  parser.add_option("--server", dest="server", default="localhost",
+                    help="Server name/address for Gerrit's SSH daemon "
+                         "[default: %default]")
+  parser.add_option("--whitespace", action="store_true",
+                    help="Treat whitespace as significant")
+
+  (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)
+  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 whitespace content changed. Some
+  # languages are more sensitive to whitespace than others, and some changes
+  # may either introduce or be intended to fix style problems specifically
+  # involving whitespace as well.
+  if options.whitespace:
+    prev_patch_ws = GetPatchId(prev_revision, consider_whitespace=True)
+    cur_patch_ws = GetPatchId(options.commit, consider_whitespace=True)
+    if cur_patch_ws.split()[0] != prev_patch_ws.split()[0]:
+      # Insert a comment into the change letting the approvers know only the
+      # whitespace changed
+      comment_msg = "\"New patchset patch-id matches previous patchset, " \
+                     "but whitespace content has changed.\""
+      comment_cmd = ['gerrit', 'approve', '--project', options.project,
+                     '--message', comment_msg, options.commit]
+      SuExec(options, options.role_user, ' '.join(comment_cmd))
+      exit(0)
+
+  # We should also 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 = "\"New patchset patch-id matches previous patchset, " \
+                   "but commit message has changed.\""
+    comment_cmd = ['gerrit', 'approve', '--project', options.project,
+                   '--message', comment_msg, options.commit]
+    SuExec(options, options.role_user, ' '.join(comment_cmd))
+    exit(0)
+
+  # Need to get all approvals on prior patch set, then suexec them onto
+  # this patchset.
+  approvals = GetApprovals(options)
+  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
+    elif approval["category_id"] == "APRV":
+      # Similarly squash old approvals
+      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]
+    SuExec(options, approval["account_id"], ' '.join(gerrit_approve_cmd))
+  exit(0)
diff --git a/setup.py b/setup.py
index 2a87690..f5f709b 100644
--- a/setup.py
+++ b/setup.py
@@ -61,6 +61,7 @@
             'notify-impact=gerritx.cmd.notify_impact:main',
             'process-cache=gerritx.cmd.process_cache:main',
             'run-mirror=gerritx.cmd.run_mirror:main',
+            'trivial-rebase=gerritx.cmd.trivial_rebase:main',
             'update-blueprint=gerritx.cmd.update_blueprint:main',
             'update-bug=gerritx.cmd.update_bug:main',
         ],