summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Pursehouse <david.pursehouse@sonymobile.com>2014-05-29 17:08:22 +0900
committerDavid Pursehouse <david.pursehouse@sonymobile.com>2014-05-29 17:15:44 +0900
commitd202b97cb67f816ee7b82f6850e8a36bf65babf9 (patch)
treefe140b22066d175f9c954ffe89e3cc2c7b802759
parent7064be15a16240c3318a9c54141387d40ecb69f5 (diff)
downloadpygerrit-d202b97cb67f816ee7b82f6850e8a36bf65babf9.tar.gz
Revert "Remove support for Gerrit over ssh"
The ssh interface is still being used and patches are being contributed [1]. It's easier to revert the removal on master than attempt to keep the support in a separate branch. This reverts commit f0b77968389966cd7bad0cac1fe7f04526eafde1. [1] https://github.com/sonyxperiadev/pygerrit/pull/21 Conflicts: README.rst example.py requirements.txt setup.py unittests.py Change-Id: I9e02208c57cb2022ff39cff6650101ed40c0519d
-rw-r--r--README.rst73
-rwxr-xr-xexample.py113
-rw-r--r--pygerrit/client.py136
-rw-r--r--pygerrit/error.py31
-rw-r--r--pygerrit/events.py331
-rw-r--r--pygerrit/models.py157
-rw-r--r--pygerrit/rest/__init__.py (renamed from pygerrit/rest.py)0
-rw-r--r--pygerrit/rest/auth.py (renamed from pygerrit/auth.py)0
-rw-r--r--pygerrit/ssh.py178
-rw-r--r--pygerrit/stream.py77
-rw-r--r--requirements.txt2
-rwxr-xr-xrest_example.py110
-rw-r--r--testdata/change-abandoned-event.txt13
-rw-r--r--testdata/change-merged-event.txt18
-rw-r--r--testdata/change-restored-event.txt13
-rw-r--r--testdata/comment-added-event.txt25
-rw-r--r--testdata/draft-published-event.txt18
-rw-r--r--testdata/invalid-json.txt4
-rw-r--r--testdata/merge-failed-event.txt19
-rw-r--r--testdata/patchset-created-event.txt18
-rw-r--r--testdata/ref-updated-event.txt7
-rw-r--r--testdata/reviewer-added-event.txt18
-rw-r--r--testdata/topic-changed-event.txt13
-rw-r--r--testdata/unhandled-event.txt3
-rw-r--r--testdata/user-defined-event.txt3
-rwxr-xr-xunittests.py289
26 files changed, 1603 insertions, 66 deletions
diff --git a/README.rst b/README.rst
index 767cd1d..7d070cb 100644
--- a/README.rst
+++ b/README.rst
@@ -7,12 +7,8 @@ Pygerrit - Client library for interacting with Gerrit Code Review
.. image:: https://pypip.in/d/pygerrit/badge.png
:target: https://crate.io/packages/pygerrit/
-`Gerrit Code Review`_ offers a feature-rich REST API. Pygerrit provides a
-simple interface for clients to interact with Gerrit via the REST API.
-
-Note that from version 0.3.0 Pygerrit no longer includes support for the Gerrit
-ssh interface. For ssh support please use version 0.2.5 or earlier.
-
+Pygerrit provides a simple interface for clients to interact with
+`Gerrit Code Review`_ via ssh or the REST API.
Prerequisites
-------------
@@ -20,7 +16,7 @@ Prerequisites
Pygerrit has been tested on Ubuntu 10.4 and Mac OSX 10.8.4, with Python 2.6.x
and 2.7.x. Support for other platforms and Python versions is not guaranteed.
-Pygerrit depends on the `requests`_ library.
+Pygerrit depends on the `paramiko_` and `requests`_ libraries.
Installation
@@ -34,6 +30,16 @@ To install pygerrit, simply::
Configuration
-------------
+For easier connection to the review server over ssh, the ssh connection
+parameters (hostname, port, username) can be given in the user's ``.ssh/config``
+file::
+
+ Host review
+ HostName review.example.net
+ Port 29418
+ User username
+
+
For easier connection to the review server over the REST API, the user's
HTTP username and password can be given in the user's ``.netrc`` file::
@@ -44,8 +50,53 @@ For instructions on how to obtain the HTTP password, refer to Gerrit's
`HTTP upload settings`_ documentation.
-Usage
------
+SSH Interface
+-------------
+
+The SSH interface can be used to run commands on the Gerrit server::
+
+ >>> from pygerrit.ssh import GerritSSHClient
+ >>> client = GerritSSHClient("review")
+ >>> result = client.run_gerrit_command("version")
+ >>> result
+ <GerritSSHCommandResult [version]>
+ >>> result.stdout
+ <paramiko.ChannelFile from <paramiko.Channel 2 (closed) -> <paramiko.Transport at 0xd2387d90L (cipher aes128-cbc, 128 bits) (active; 0 open channel(s))>>>
+ >>> result.stdout.read()
+ 'gerrit version 2.6.1\n'
+ >>>
+
+Event Stream
+------------
+
+Gerrit offers a ``stream-events`` command that is run over ssh, and returns back
+a stream of events (new change uploaded, change merged, comment added, etc) as
+JSON text.
+
+This library handles the parsing of the JSON text from the event stream,
+encapsulating the data in event objects (Python classes), and allowing the
+client to fetch them from a queue. It also allows users to easily add handling
+of custom event types, for example if they are running a customised Gerrit
+installation with non-standard events::
+
+ >>> from pygerrit.client import GerritClient
+ >>> client = GerritClient("review")
+ >>> client.gerrit_version()
+ '2.6.1'
+ >>> client.start_event_stream()
+ >>> client.get_event()
+ <CommentAddedEvent>: <Change 12345, platform/packages/apps/Example, master> <Patchset 1, 5c4b2f76297f04fbab77eb8c3462e087bc4b6f90> <Account Bob Example (bob.example@example.com)>
+ >>> client.get_event()
+ <CommentAddedEvent>: <Change 67890, platform/frameworks/example, master> <Patchset 2, c7d4f9956c80b1df66a66d66dea3960e71de4910> <Account John Example (john.example@example.com)>
+ >>> client.stop_event_stream()
+ >>>
+
+
+Refer to the `example`_ script for a more detailed example of how the SSH
+event stream interface works.
+
+REST API
+--------
This simple example shows how to get the user's open changes, authenticating
to Gerrit via HTTP Digest authentication using an explicitly given username and
@@ -58,7 +109,7 @@ password::
>>> changes = rest.get("/changes/?q=owner:self%20status:open")
-Refer to the `example`_ script for a more detailed example of how the
+Refer to the `rest_example`_ script for a more detailed example of how the
REST API interface works.
@@ -74,6 +125,8 @@ license details.
.. _`Gerrit Code Review`: https://code.google.com/p/gerrit/
.. _`requests`: https://github.com/kennethreitz/requests
+.. _`paramiko`: https://github.com/paramiko/paramiko
.. _example: https://github.com/sonyxperiadev/pygerrit/blob/master/example.py
+.. _rest_example: https://github.com/sonyxperiadev/pygerrit/blob/master/rest_example.py
.. _`HTTP upload settings`: https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/user-upload.html#http
.. _LICENSE: https://github.com/sonyxperiadev/pygerrit/blob/master/LICENSE
diff --git a/example.py b/example.py
index f53820a..99fe940 100755
--- a/example.py
+++ b/example.py
@@ -3,7 +3,7 @@
# The MIT License
#
-# Copyright 2013 Sony Mobile Communications. All rights reserved.
+# Copyright 2012 Sony Mobile Communications. All rights reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -23,86 +23,89 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
-""" Example of using the Gerrit client REST API. """
+""" Example of using the Gerrit client class. """
import argparse
import logging
import sys
+from threading import Event
+import time
-from requests.auth import HTTPBasicAuth, HTTPDigestAuth
-from requests.exceptions import RequestException
-try:
- from requests_kerberos import HTTPKerberosAuth, OPTIONAL
- _kerberos_support = True
-except ImportError:
- _kerberos_support = False
-
-from pygerrit.rest import GerritRestAPI
-from pygerrit.auth import HTTPDigestAuthFromNetrc, HTTPBasicAuthFromNetrc
+from pygerrit.client import GerritClient
+from pygerrit.error import GerritError
+from pygerrit.events import ErrorEvent
def _main():
- descr = 'Send request using Gerrit HTTP API'
+ descr = 'Send request using Gerrit ssh API'
parser = argparse.ArgumentParser(
description=descr,
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
- parser.add_argument('-g', '--gerrit-url', dest='gerrit_url',
- required=True,
- help='gerrit server url')
- parser.add_argument('-b', '--basic-auth', dest='basic_auth',
- action='store_true',
- help='use basic auth instead of digest')
- if _kerberos_support:
- parser.add_argument('-k', '--kerberos-auth', dest='kerberos_auth',
- action='store_true',
- help='use kerberos auth')
+ parser.add_argument('-g', '--gerrit-hostname', dest='hostname',
+ default='review',
+ help='gerrit server hostname')
+ parser.add_argument('-p', '--port', dest='port',
+ type=int, default=29418,
+ help='port number')
parser.add_argument('-u', '--username', dest='username',
help='username')
- parser.add_argument('-p', '--password', dest='password',
- help='password')
- parser.add_argument('-n', '--netrc', dest='netrc',
+ parser.add_argument('-b', '--blocking', dest='blocking',
action='store_true',
- help='Use credentials from netrc')
+ help='block on event get')
+ parser.add_argument('-t', '--timeout', dest='timeout',
+ default=None, type=int,
+ metavar='SECONDS',
+ help='timeout for blocking event get')
parser.add_argument('-v', '--verbose', dest='verbose',
action='store_true',
help='enable verbose (debug) logging')
+ parser.add_argument('-i', '--ignore-stream-errors', dest='ignore',
+ action='store_true',
+ help='do not exit when an error event is received')
options = parser.parse_args()
+ if options.timeout and not options.blocking:
+ parser.error('Can only use --timeout with --blocking')
level = logging.DEBUG if options.verbose else logging.INFO
logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s',
level=level)
- if _kerberos_support and options.kerberos_auth:
- if options.username or options.password \
- or options.basic_auth or options.netrc:
- parser.error("--kerberos-auth may not be used together with "
- "--username, --password, --basic-auth or --netrc")
- auth = HTTPKerberosAuth(mutual_authentication=OPTIONAL)
- elif options.username and options.password:
- if options.netrc:
- logging.warning("--netrc option ignored")
- if options.basic_auth:
- auth = HTTPBasicAuth(options.username, options.password)
- else:
- auth = HTTPDigestAuth(options.username, options.password)
- elif options.netrc:
- if options.basic_auth:
- auth = HTTPBasicAuthFromNetrc(url=options.gerrit_url)
- else:
- auth = HTTPDigestAuthFromNetrc(url=options.gerrit_url)
- else:
- auth = None
-
- rest = GerritRestAPI(url=options.gerrit_url, auth=auth)
+ try:
+ gerrit = GerritClient(host=options.hostname,
+ username=options.username,
+ port=options.port)
+ logging.info("Connected to Gerrit version [%s]",
+ gerrit.gerrit_version())
+ gerrit.start_event_stream()
+ except GerritError as err:
+ logging.error("Gerrit error: %s", err)
+ return 1
+ errors = Event()
try:
- changes = rest.get("/changes/?q=owner:self%20status:open")
- logging.info("%d changes", len(changes))
- for change in changes:
- logging.info(change['change_id'])
- except RequestException as err:
- logging.error("Error: %s", str(err))
+ while True:
+ event = gerrit.get_event(block=options.blocking,
+ timeout=options.timeout)
+ if event:
+ logging.info("Event: %s", event)
+ if isinstance(event, ErrorEvent) and not options.ignore:
+ logging.error(event.error)
+ errors.set()
+ break
+ else:
+ logging.info("No event")
+ if not options.blocking:
+ time.sleep(1)
+ except KeyboardInterrupt:
+ logging.info("Terminated by user")
+ finally:
+ logging.debug("Stopping event stream...")
+ gerrit.stop_event_stream()
+
+ if errors.isSet():
+ logging.error("Exited with error")
+ return 1
if __name__ == "__main__":
sys.exit(_main())
diff --git a/pygerrit/client.py b/pygerrit/client.py
new file mode 100644
index 0000000..4095914
--- /dev/null
+++ b/pygerrit/client.py
@@ -0,0 +1,136 @@
+# The MIT License
+#
+# Copyright 2012 Sony Mobile Communications. All rights reserved.
+#
+# 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.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+""" Gerrit client interface. """
+
+from json import JSONDecoder
+from Queue import Queue, Empty, Full
+
+from . import escape_string
+from .error import GerritError
+from .events import GerritEventFactory
+from .models import Change
+from .ssh import GerritSSHClient
+from .stream import GerritStream
+
+
+class GerritClient(object):
+
+ """ Gerrit client interface. """
+
+ def __init__(self, host, username=None, port=None):
+ self._factory = GerritEventFactory()
+ self._events = Queue()
+ self._stream = None
+ self._ssh_client = GerritSSHClient(host, username=username, port=port)
+
+ def gerrit_version(self):
+ """ Return the version of Gerrit that is connected to. """
+ return self._ssh_client.get_remote_version()
+
+ def gerrit_info(self):
+ """ Return the username, and version of Gerrit that is connected to. """
+ return self._ssh_client.get_remote_info()
+
+ def run_command(self, command):
+ """ Run the command. Return the result. """
+ if not isinstance(command, basestring):
+ raise ValueError("command must be a string")
+ return self._ssh_client.run_gerrit_command(command)
+
+ def query(self, term):
+ """ Run `gerrit query` with the given `term`.
+
+ Return a list of results as `Change` objects.
+
+ Raise `ValueError` if `term` is not a string.
+
+ """
+ results = []
+ command = ["query", "--current-patch-set", "--all-approvals",
+ "--format JSON", "--commit-message"]
+
+ if not isinstance(term, basestring):
+ raise ValueError("term must be a string")
+
+ command.append(escape_string(term))
+ result = self._ssh_client.run_gerrit_command(" ".join(command))
+ decoder = JSONDecoder()
+ for line in result.stdout.read().splitlines():
+ # Gerrit's response to the query command contains one or more
+ # lines of JSON-encoded strings. The last one is a status
+ # dictionary containing the key "type" whose value indicates
+ # whether or not the operation was successful.
+ # According to http://goo.gl/h13HD it should be safe to use the
+ # presence of the "type" key to determine whether the dictionary
+ # represents a change or if it's the query status indicator.
+ try:
+ data = decoder.decode(line)
+ except ValueError as err:
+ raise GerritError("Query returned invalid data: %s", err)
+ if "type" in data and data["type"] == "error":
+ raise GerritError("Query error: %s" % data["message"])
+ elif "project" in data:
+ results.append(Change(data))
+ return results
+
+ def start_event_stream(self):
+ """ Start streaming events from `gerrit stream-events`. """
+ if not self._stream:
+ self._stream = GerritStream(self, ssh_client=self._ssh_client)
+ self._stream.start()
+
+ def stop_event_stream(self):
+ """ Stop streaming events from `gerrit stream-events`."""
+ if self._stream:
+ self._stream.stop()
+ self._stream.join()
+ self._stream = None
+ with self._events.mutex:
+ self._events.queue.clear()
+
+ def get_event(self, block=True, timeout=None):
+ """ Get the next event from the queue.
+
+ Return a `GerritEvent` instance, or None if:
+ - `block` is False and there is no event available in the queue, or
+ - `block` is True and no event is available within the time
+ specified by `timeout`.
+
+ """
+ try:
+ return self._events.get(block, timeout)
+ except Empty:
+ return None
+
+ def put_event(self, data):
+ """ Create event from `data` and add it to the queue.
+
+ Raise GerritError if the queue is full, or the factory could not
+ create the event.
+
+ """
+ try:
+ event = self._factory.create(data)
+ self._events.put(event)
+ except Full:
+ raise GerritError("Unable to add event: queue is full")
diff --git a/pygerrit/error.py b/pygerrit/error.py
new file mode 100644
index 0000000..b500812
--- /dev/null
+++ b/pygerrit/error.py
@@ -0,0 +1,31 @@
+# The MIT License
+#
+# Copyright 2011 Sony Ericsson Mobile Communications. All rights reserved.
+# Copyright 2012 Sony Mobile Communications. All rights reserved.
+#
+# 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.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+""" Error classes. """
+
+
+class GerritError(Exception):
+
+ """ Raised when something goes wrong in Gerrit handling. """
+
+ pass
diff --git a/pygerrit/events.py b/pygerrit/events.py
new file mode 100644
index 0000000..c6563cd
--- /dev/null
+++ b/pygerrit/events.py
@@ -0,0 +1,331 @@
+# The MIT License
+#
+# Copyright 2011 Sony Ericsson Mobile Communications. All rights reserved.
+# Copyright 2012 Sony Mobile Communications. All rights reserved.
+#
+# 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.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+""" Gerrit event classes. """
+
+import json
+import logging
+
+from .error import GerritError
+from .models import Account, Approval, Change, Patchset, RefUpdate
+
+
+class GerritEventFactory(object):
+
+ """ Gerrit event factory. """
+
+ _events = {}
+
+ @classmethod
+ def register(cls, name):
+ """ Decorator to register the event identified by `name`.
+
+ Return the decorated class.
+
+ Raise GerritError if the event is already registered.
+
+ """
+
+ def decorate(klazz):
+ """ Decorator. """
+ if name in cls._events:
+ raise GerritError("Duplicate event: %s" % name)
+ cls._events[name] = [klazz.__module__, klazz.__name__]
+ klazz.name = name
+ return klazz
+ return decorate
+
+ @classmethod
+ def create(cls, data):
+ """ Create a new event instance.
+
+ Return an instance of the `GerritEvent` subclass after converting
+ `data` to json.
+
+ Raise GerritError if json parsed from `data` does not contain a `type`
+ key.
+
+ """
+ try:
+ json_data = json.loads(data)
+ except ValueError as err:
+ logging.debug("Failed to load json data: %s: [%s]", str(err), data)
+ json_data = json.loads(ErrorEvent.error_json(err))
+
+ if not "type" in json_data:
+ raise GerritError("`type` not in json_data")
+ name = json_data["type"]
+ if not name in cls._events:
+ name = 'unhandled-event'
+ event = cls._events[name]
+ module_name = event[0]
+ class_name = event[1]
+ module = __import__(module_name, fromlist=[module_name])
+ klazz = getattr(module, class_name)
+ return klazz(json_data)
+
+
+class GerritEvent(object):
+
+ """ Gerrit event base class. """
+
+ def __init__(self, json_data):
+ self.json = json_data
+
+
+@GerritEventFactory.register("unhandled-event")
+class UnhandledEvent(GerritEvent):
+
+ """ Unknown event type received in json data from Gerrit's event stream. """
+
+ def __init__(self, json_data):
+ super(UnhandledEvent, self).__init__(json_data)
+
+ def __repr__(self):
+ return u"<UnhandledEvent>"
+
+
+@GerritEventFactory.register("error-event")
+class ErrorEvent(GerritEvent):
+
+ """ Error occurred when processing json data from Gerrit's event stream. """
+
+ def __init__(self, json_data):
+ super(ErrorEvent, self).__init__(json_data)
+ self.error = json_data["error"]
+
+ @classmethod
+ def error_json(cls, error):
+ """ Return a json string for the `error`. """
+ return '{"type":"error-event",' \
+ '"error":"%s"}' % str(error)
+
+ def __repr__(self):
+ return u"<ErrorEvent: %s>" % self.error
+
+
+@GerritEventFactory.register("patchset-created")
+class PatchsetCreatedEvent(GerritEvent):
+
+ """ Gerrit "patchset-created" event. """
+
+ def __init__(self, json_data):
+ super(PatchsetCreatedEvent, self).__init__(json_data)
+ try:
+ self.change = Change(json_data["change"])
+ self.patchset = Patchset(json_data["patchSet"])
+ self.uploader = Account(json_data["uploader"])
+ except KeyError as e:
+ raise GerritError("PatchsetCreatedEvent: %s" % e)
+
+ def __repr__(self):
+ return u"<PatchsetCreatedEvent>: %s %s %s" % (self.change,
+ self.patchset,
+ self.uploader)
+
+
+@GerritEventFactory.register("draft-published")
+class DraftPublishedEvent(GerritEvent):
+
+ """ Gerrit "draft-published" event. """
+
+ def __init__(self, json_data):
+ super(DraftPublishedEvent, self).__init__(json_data)
+ try:
+ self.change = Change(json_data["change"])
+ self.patchset = Patchset(json_data["patchSet"])
+ self.uploader = Account(json_data["uploader"])
+ except KeyError as e:
+ raise GerritError("DraftPublishedEvent: %s" % e)
+
+ def __repr__(self):
+ return u"<DraftPublishedEvent>: %s %s %s" % (self.change,
+ self.patchset,
+ self.uploader)
+
+
+@GerritEventFactory.register("comment-added")
+class CommentAddedEvent(GerritEvent):
+
+ """ Gerrit "comment-added" event. """
+
+ def __init__(self, json_data):
+ super(CommentAddedEvent, self).__init__(json_data)
+ try:
+ self.change = Change(json_data["change"])
+ self.patchset = Patchset(json_data["patchSet"])
+ self.author = Account(json_data["author"])
+ self.approvals = []
+ if "approvals" in json_data:
+ for approval in json_data["approvals"]:
+ self.approvals.append(Approval(approval))
+ self.comment = json_data["comment"]
+ except (KeyError, ValueError) as e:
+ raise GerritError("CommentAddedEvent: %s" % e)
+
+ def __repr__(self):
+ return u"<CommentAddedEvent>: %s %s %s" % (self.change,
+ self.patchset,
+ self.author)
+
+
+@GerritEventFactory.register("change-merged")
+class ChangeMergedEvent(GerritEvent):
+
+ """ Gerrit "change-merged" event. """
+
+ def __init__(self, json_data):
+ super(ChangeMergedEvent, self).__init__(json_data)
+ try:
+ self.change = Change(json_data["change"])
+ self.patchset = Patchset(json_data["patchSet"])
+ self.submitter = Account(json_data["submitter"])
+ except KeyError as e:
+ raise GerritError("ChangeMergedEvent: %s" % e)
+
+ def __repr__(self):
+ return u"<ChangeMergedEvent>: %s %s %s" % (self.change,
+ self.patchset,
+ self.submitter)
+
+
+@GerritEventFactory.register("merge-failed")
+class MergeFailedEvent(GerritEvent):
+
+ """ Gerrit "merge-failed" event. """
+
+ def __init__(self, json_data):
+ super(MergeFailedEvent, self).__init__(json_data)
+ try:
+ self.change = Change(json_data["change"])
+ self.patchset = Patchset(json_data["patchSet"])
+ self.submitter = Account(json_data["submitter"])
+ if 'reason' in json_data:
+ self.reason = json_data["reason"]
+ except KeyError as e:
+ raise GerritError("MergeFailedEvent: %s" % e)
+
+ def __repr__(self):
+ return u"<MergeFailedEvent>: %s %s %s" % (self.change,
+ self.patchset,
+ self.submitter)
+
+
+@GerritEventFactory.register("change-abandoned")
+class ChangeAbandonedEvent(GerritEvent):
+
+ """ Gerrit "change-abandoned" event. """
+
+ def __init__(self, json_data):
+ super(ChangeAbandonedEvent, self).__init__(json_data)
+ try:
+ self.change = Change(json_data["change"])
+ self.abandoner = Account(json_data["abandoner"])
+ if 'reason' in json_data:
+ self.reason = json_data["reason"]
+ except KeyError as e:
+ raise GerritError("ChangeAbandonedEvent: %s" % e)
+
+ def __repr__(self):
+ return u"<ChangeAbandonedEvent>: %s %s" % (self.change,
+ self.abandoner)
+
+
+@GerritEventFactory.register("change-restored")
+class ChangeRestoredEvent(GerritEvent):
+
+ """ Gerrit "change-restored" event. """
+
+ def __init__(self, json_data):
+ super(ChangeRestoredEvent, self).__init__(json_data)
+ try:
+ self.change = Change(json_data["change"])
+ self.restorer = Account(json_data["restorer"])
+ if 'reason' in json_data:
+ self.reason = json_data["reason"]
+ except KeyError as e:
+ raise GerritError("ChangeRestoredEvent: %s" % e)
+
+ def __repr__(self):
+ return u"<ChangeRestoredEvent>: %s %s" % (self.change,
+ self.restorer)
+
+
+@GerritEventFactory.register("ref-updated")
+class RefUpdatedEvent(GerritEvent):
+
+ """ Gerrit "ref-updated" event. """
+
+ def __init__(self, json_data):
+ super(RefUpdatedEvent, self).__init__(json_data)
+ try:
+ self.ref_update = RefUpdate(json_data["refUpdate"])
+ self.submitter = Account.from_json(json_data, "submitter")
+ except KeyError as e:
+ raise GerritError("RefUpdatedEvent: %s" % e)
+
+ def __repr__(self):
+ return u"<RefUpdatedEvent>: %s %s" % (self.ref_update, self.submitter)
+
+
+@GerritEventFactory.register("reviewer-added")
+class ReviewerAddedEvent(GerritEvent):
+
+ """ Gerrit "reviewer-added" event. """
+
+ def __init__(self, json_data):
+ super(ReviewerAddedEvent, self).__init__(json_data)
+ try:
+ self.change = Change(json_data["change"])
+ self.patchset = Patchset.from_json(json_data)
+ self.reviewer = Account(json_data["reviewer"])
+ except KeyError as e:
+ raise GerritError("ReviewerAddedEvent: %s" % e)
+
+ def __repr__(self):
+ return u"<ReviewerAddedEvent>: %s %s %s" % (self.change,
+ self.patchset,
+ self.reviewer)
+
+
+@GerritEventFactory.register("topic-changed")
+class TopicChangedEvent(GerritEvent):
+
+ """ Gerrit "topic-changed" event. """
+
+ def __init__(self, json_data):
+ super(TopicChangedEvent, self).__init__(json_data)
+ try:
+ self.change = Change(json_data["change"])
+ self.changer = Account(json_data["changer"])
+ if "oldTopic" in json_data:
+ self.oldtopic = json_data["oldTopic"]
+ else:
+ self.oldtopic = ""
+ except KeyError as e:
+ raise GerritError("TopicChangedEvent: %s" % e)
+
+ def __repr__(self):
+ return u"<TopicChangedEvent>: %s %s [%s]" % (self.change,
+ self.changer,
+ self.oldtopic)
diff --git a/pygerrit/models.py b/pygerrit/models.py
new file mode 100644
index 0000000..17dc7ee
--- /dev/null
+++ b/pygerrit/models.py
@@ -0,0 +1,157 @@
+# The MIT License
+#
+# Copyright 2011 Sony Ericsson Mobile Communications. All rights reserved.
+# Copyright 2012 Sony Mobile Communications. All rights reserved.
+#
+# 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.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+""" Models for Gerrit JSON data. """
+
+from . import from_json
+
+
+class Account(object):
+
+ """ Gerrit user account (name and email address). """
+
+ def __init__(self, json_data):
+ self.name = from_json(json_data, "name")
+ self.email = from_json(json_data, "email")
+ self.username = from_json(json_data, "username")
+
+ def __repr__(self):
+ return u"<Account %s%s>" % (self.name,
+ " (%s)" % self.email if self.email else "")
+
+ @staticmethod
+ def from_json(json_data, key):
+ """ Create an Account instance.
+
+ Return an instance of Account initialised with values from `key`
+ in `json_data`, or None if `json_data` does not contain `key`.
+
+ """
+ if key in json_data:
+ return Account(json_data[key])
+ return None
+
+
+class Change(object):
+
+ """ Gerrit change. """
+
+ def __init__(self, json_data):
+ self.project = from_json(json_data, "project")
+ self.branch = from_json(json_data, "branch")
+ self.topic = from_json(json_data, "topic")
+ self.change_id = from_json(json_data, "id")
+ self.number = from_json(json_data, "number")
+ self.subject = from_json(json_data, "subject")
+ self.url = from_json(json_data, "url")
+ self.owner = Account.from_json(json_data, "owner")
+ self.sortkey = from_json(json_data, "sortKey")
+ self.status = from_json(json_data, "status")
+ self.current_patchset = CurrentPatchset.from_json(json_data)
+
+ def __repr__(self):
+ return u"<Change %s, %s, %s>" % (self.number, self.project, self.branch)
+
+
+class Patchset(object):
+
+ """ Gerrit patch set. """
+
+ def __init__(self, json_data):
+ self.number = from_json(json_data, "number")
+ self.revision = from_json(json_data, "revision")
+ self.ref = from_json(json_data, "ref")
+ self.uploader = Account.from_json(json_data, "uploader")
+
+ def __repr__(self):
+ return u"<Patchset %s, %s>" % (self.number, self.revision)
+
+ @staticmethod
+ def from_json(json_data):
+ r""" Create a Patchset instance.
+
+ Return an instance of Patchset initialised with values from "patchSet"
+ in `json_data`, or None if `json_data` does not contain "patchSet".
+
+ """
+ if "patchSet" in json_data:
+ return Patchset(json_data["patchSet"])
+ return None
+
+
+class CurrentPatchset(Patchset):
+
+ """ Gerrit current patch set. """
+
+ def __init__(self, json_data):
+ super(CurrentPatchset, self).__init__(json_data)
+ self.author = Account.from_json(json_data, "author")
+ self.approvals = []
+ if "approvals" in json_data:
+ for approval in json_data["approvals"]:
+ self.approvals.append(Approval(approval))
+
+ def __repr__(self):
+ return u"<CurrentPatchset %s, %s>" % (self.number, self.revision)
+
+ @staticmethod
+ def from_json(json_data):
+ r""" Create a CurrentPatchset instance.
+
+ Return an instance of CurrentPatchset initialised with values from
+ "currentPatchSet" in `json_data`, or None if `json_data` does not
+ contain "currentPatchSet".
+
+ """
+ if "currentPatchSet" in json_data:
+ return CurrentPatchset(json_data["currentPatchSet"])
+ return None
+
+
+class Approval(object):
+
+ """ Gerrit approval (verified, code review, etc). """
+
+ def __init__(self, json_data):
+ self.category = from_json(json_data, "type")
+ self.value = from_json(json_data, "value")
+ self.description = from_json(json_data, "description")
+ self.approver = Account.from_json(json_data, "by")
+
+ def __repr__(self):
+ return u"<Approval %s %s>" % (self.description, self.value)
+
+
+class RefUpdate(object):
+
+ """ Gerrit ref update. """
+
+ def __init__(self, json_data):
+ self.oldrev = from_json(json_data, "oldRev")
+ self.newrev = from_json(json_data, "newRev")
+ self.refname = from_json(json_data, "refName")
+ self.project = from_json(json_data, "project")
+
+ def __repr__(self):
+ return "<RefUpdate %s %s %s %s>" % \
+ (self.project, self.refname, self.oldrev, self.newrev)
diff --git a/pygerrit/rest.py b/pygerrit/rest/__init__.py
index 5b197a0..5b197a0 100644
--- a/pygerrit/rest.py
+++ b/pygerrit/rest/__init__.py
diff --git a/pygerrit/auth.py b/pygerrit/rest/auth.py
index c43c3fa..c43c3fa 100644
--- a/pygerrit/auth.py
+++ b/pygerrit/rest/auth.py
diff --git a/pygerrit/ssh.py b/pygerrit/ssh.py
new file mode 100644
index 0000000..2933ddf
--- /dev/null
+++ b/pygerrit/ssh.py
@@ -0,0 +1,178 @@
+# The MIT License
+#
+# Copyright 2012 Sony Mobile Communications. All rights reserved.
+#
+# 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.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+""" Gerrit SSH Client. """
+
+from os.path import abspath, expanduser, isfile
+import re
+import socket
+from threading import Event, Lock
+
+from .error import GerritError
+
+from paramiko import SSHClient, SSHConfig
+from paramiko.ssh_exception import SSHException
+
+
+def _extract_version(version_string, pattern):
+ """ Extract the version from `version_string` using `pattern`.
+
+ Return the version as a string, with leading/trailing whitespace
+ stripped.
+
+ """
+ if version_string:
+ match = pattern.match(version_string.strip())
+ if match:
+ return match.group(1)
+ return ""
+
+
+class GerritSSHCommandResult(object):
+
+ """ Represents the results of a Gerrit command run over SSH. """
+
+ def __init__(self, command, stdin, stdout, stderr):
+ self.command = command
+ self.stdin = stdin
+ self.stdout = stdout
+ self.stderr = stderr
+
+ def __repr__(self):
+ return "<GerritSSHCommandResult [%s]>" % self.command
+
+
+class GerritSSHClient(SSHClient):
+
+ """ Gerrit SSH Client, wrapping the paramiko SSH Client. """
+
+ def __init__(self, hostname, username=None, port=None):
+ """ Initialise and connect to SSH. """
+ super(GerritSSHClient, self).__init__()
+ self.remote_version = None
+ self.hostname = hostname
+ self.username = username
+ self.key_filename = None
+ self.port = port
+ self.connected = Event()
+ self.lock = Lock()
+
+ def _configure(self):
+ """ Configure the ssh parameters from the config file. """
+ configfile = expanduser("~/.ssh/config")
+ if not isfile(configfile):
+ raise GerritError("ssh config file '%s' does not exist" %
+ configfile)
+
+ config = SSHConfig()
+ config.parse(open(configfile))
+ data = config.lookup(self.hostname)
+ if not data:
+ raise GerritError("No ssh config for host %s" % self.hostname)
+ if not 'hostname' in data or not 'port' in data or not 'user' in data:
+ raise GerritError("Missing configuration data in %s" % configfile)
+ self.hostname = data['hostname']
+ self.username = data['user']
+ if 'identityfile' in data:
+ key_filename = abspath(expanduser(data['identityfile'][0]))
+ if not isfile(key_filename):
+ raise GerritError("Identity file '%s' does not exist" %
+ key_filename)
+ self.key_filename = key_filename
+ try:
+ self.port = int(data['port'])
+ except ValueError:
+ raise GerritError("Invalid port: %s" % data['port'])
+
+ def _do_connect(self):
+ """ Connect to the remote. """
+ self.load_system_host_keys()
+ if self.username is None or self.port is None:
+ self._configure()
+ try:
+ self.connect(hostname=self.hostname,
+ port=self.port,
+ username=self.username,
+ key_filename=self.key_filename)
+ except socket.error as e:
+ raise GerritError("Failed to connect to server: %s" % e)
+
+ try:
+ version_string = self._transport.remote_version
+ pattern = re.compile(r'^.*GerritCodeReview_([a-z0-9-\.]*) .*$')
+ self.remote_version = _extract_version(version_string, pattern)
+ except AttributeError:
+ self.remote_version = None
+
+ def _connect(self):
+ """ Connect to the remote if not already connected. """
+ if not self.connected.is_set():
+ try:
+ self.lock.acquire()
+ # Another thread may have connected while we were
+ # waiting to acquire the lock
+ if not self.connected.is_set():
+ self._do_connect()
+ self.connected.set()
+ except GerritError:
+ raise
+ finally:
+ self.lock.release()
+
+ def get_remote_version(self):
+ """ Return the version of the remote Gerrit server. """
+ if self.remote_version is None:
+ result = self.run_gerrit_command("version")
+ version_string = result.stdout.read()
+ pattern = re.compile(r'^gerrit version (.*)$')
+ self.remote_version = _extract_version(version_string, pattern)
+ return self.remote_version
+
+ def get_remote_info(self):
+ """ Return the username, and version of the remote Gerrit server. """
+ version = self.get_remote_version()
+ return (self.username, version)
+
+ def run_gerrit_command(self, command):
+ """ Run the given command.
+
+ Make sure we're connected to the remote server, and run `command`.
+
+ Return the results as a `GerritSSHCommandResult`.
+
+ Raise `ValueError` if `command` is not a string, or `GerritError` if
+ command execution fails.
+
+ """
+ if not isinstance(command, basestring):
+ raise ValueError("command must be a string")
+ gerrit_command = "gerrit " + command
+
+ self._connect()
+ try:
+ stdin, stdout, stderr = self.exec_command(gerrit_command,
+ bufsize=1,
+ timeout=None,
+ get_pty=False)
+ except SSHException as err:
+ raise GerritError("Command execution error: %s" % err)
+ return GerritSSHCommandResult(command, stdin, stdout, stderr)
diff --git a/pygerrit/stream.py b/pygerrit/stream.py
new file mode 100644
index 0000000..1504dde
--- /dev/null
+++ b/pygerrit/stream.py
@@ -0,0 +1,77 @@
+# The MIT License
+#
+# Copyright 2012 Sony Mobile Communications. All rights reserved.
+#
+# 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.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+""" Gerrit event stream interface.
+
+Class to listen to the Gerrit event stream and dispatch events.
+
+"""
+
+from threading import Thread, Event
+
+from .events import ErrorEvent
+
+
+class GerritStream(Thread):
+
+ """ Gerrit events stream handler. """
+
+ def __init__(self, gerrit, ssh_client):
+ Thread.__init__(self)
+ self.daemon = True
+ self._gerrit = gerrit
+ self._ssh_client = ssh_client
+ self._stop = Event()
+ self._channel = None
+
+ def stop(self):
+ """ Stop the thread. """
+ self._stop.set()
+ if self._channel is not None:
+ self._channel.close()
+
+ def _error_event(self, error):
+ """ Dispatch `error` to the Gerrit client. """
+ self._gerrit.put_event(ErrorEvent.error_json(error))
+
+ def run(self):
+ """ Listen to the stream and send events to the client. """
+ channel = self._ssh_client.get_transport().open_session()
+ self._channel = channel
+ channel.exec_command("gerrit stream-events")
+ stdout = channel.makefile()
+ stderr = channel.makefile_stderr()
+ while not self._stop.is_set():
+ try:
+ if channel.exit_status_ready():
+ if channel.recv_stderr_ready():
+ error = stderr.readline().strip()
+ else:
+ error = "Remote server connection closed"
+ self._error_event(error)
+ self._stop.set()
+ else:
+ data = stdout.readline()
+ self._gerrit.put_event(data)
+ except Exception as e: # pylint: disable=W0703
+ self._error_event(repr(e))
+ self._stop.set()
diff --git a/requirements.txt b/requirements.txt
index de2c0fe..d7b30e5 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,4 @@
+paramiko==1.11.0
pbr==0.8.0
+pycrypto==2.3
requests==2.2.1
diff --git a/rest_example.py b/rest_example.py
new file mode 100755
index 0000000..1b69c00
--- /dev/null
+++ b/rest_example.py
@@ -0,0 +1,110 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# The MIT License
+#
+# Copyright 2013 Sony Mobile Communications. All rights reserved.
+#
+# 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.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+""" Example of using the Gerrit client REST API. """
+
+import argparse
+import logging
+import sys
+
+from requests.auth import HTTPBasicAuth, HTTPDigestAuth
+from requests.exceptions import RequestException
+try:
+ # pylint: disable=F0401
+ from requests_kerberos import HTTPKerberosAuth, OPTIONAL
+ # pylint: enable=F0401
+ _kerberos_support = True
+except ImportError:
+ _kerberos_support = False
+
+from pygerrit.rest import GerritRestAPI
+from pygerrit.rest.auth import HTTPDigestAuthFromNetrc, HTTPBasicAuthFromNetrc
+
+
+def _main():
+ descr = 'Send request using Gerrit HTTP API'
+ parser = argparse.ArgumentParser(
+ description=descr,
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter)
+ parser.add_argument('-g', '--gerrit-url', dest='gerrit_url',
+ required=True,
+ help='gerrit server url')
+ parser.add_argument('-b', '--basic-auth', dest='basic_auth',
+ action='store_true',
+ help='use basic auth instead of digest')
+ if _kerberos_support:
+ parser.add_argument('-k', '--kerberos-auth', dest='kerberos_auth',
+ action='store_true',
+ help='use kerberos auth')
+ parser.add_argument('-u', '--username', dest='username',
+ help='username')
+ parser.add_argument('-p', '--password', dest='password',
+ help='password')
+ parser.add_argument('-n', '--netrc', dest='netrc',
+ action='store_true',
+ help='Use credentials from netrc')
+ parser.add_argument('-v', '--verbose', dest='verbose',
+ action='store_true',
+ help='enable verbose (debug) logging')
+
+ options = parser.parse_args()
+
+ level = logging.DEBUG if options.verbose else logging.INFO
+ logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s',
+ level=level)
+
+ if _kerberos_support and options.kerberos_auth:
+ if options.username or options.password \
+ or options.basic_auth or options.netrc:
+ parser.error("--kerberos-auth may not be used together with "
+ "--username, --password, --basic-auth or --netrc")
+ auth = HTTPKerberosAuth(mutual_authentication=OPTIONAL)
+ elif options.username and options.password:
+ if options.netrc:
+ logging.warning("--netrc option ignored")
+ if options.basic_auth:
+ auth = HTTPBasicAuth(options.username, options.password)
+ else:
+ auth = HTTPDigestAuth(options.username, options.password)
+ elif options.netrc:
+ if options.basic_auth:
+ auth = HTTPBasicAuthFromNetrc(url=options.gerrit_url)
+ else:
+ auth = HTTPDigestAuthFromNetrc(url=options.gerrit_url)
+ else:
+ auth = None
+
+ rest = GerritRestAPI(url=options.gerrit_url, auth=auth)
+
+ try:
+ changes = rest.get("/changes/?q=owner:self%20status:open")
+ logging.info("%d changes", len(changes))
+ for change in changes:
+ logging.info(change['change_id'])
+ except RequestException as err:
+ logging.error("Error: %s", str(err))
+
+if __name__ == "__main__":
+ sys.exit(_main())
diff --git a/testdata/change-abandoned-event.txt b/testdata/change-abandoned-event.txt
new file mode 100644
index 0000000..879ea28
--- /dev/null
+++ b/testdata/change-abandoned-event.txt
@@ -0,0 +1,13 @@
+{"type":"change-abandoned",
+ "change":{"project":"project-name",
+ "branch":"branch-name",
+ "topic":"topic-name",
+ "id":"Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+ "number":"123456",
+ "subject":"Commit message subject",
+ "owner":{"name":"Owner Name",
+ "email":"owner@example.com"},
+ "url":"http://review.example.com/123456"},
+ "abandoner":{"name":"Abandoner Name",
+ "email":"abandoner@example.com"},
+ "reason":"Abandon reason"}
diff --git a/testdata/change-merged-event.txt b/testdata/change-merged-event.txt
new file mode 100644
index 0000000..4da678a
--- /dev/null
+++ b/testdata/change-merged-event.txt
@@ -0,0 +1,18 @@
+{"type":"change-merged",
+ "change":{"project":"project-name",
+ "branch":"branch-name",
+ "topic":"topic-name",
+ "id":"Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+ "number":"123456",
+ "subject":"Commit message subject",
+ "owner":{"name":"Owner Name",
+ "email":"owner@example.com"},
+ "url":"http://review.example.com/123456"},
+ "patchSet":{"number":"4",
+ "revision":"deadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+ "ref":"refs/changes/56/123456/4",
+ "uploader":{"name":"Uploader Name",
+ "email":"uploader@example.com"},
+ "createdOn":1341370514},
+ "submitter":{"name":"Submitter Name",
+ "email":"submitter@example.com"}}
diff --git a/testdata/change-restored-event.txt b/testdata/change-restored-event.txt
new file mode 100644
index 0000000..e0300a8
--- /dev/null
+++ b/testdata/change-restored-event.txt
@@ -0,0 +1,13 @@
+{"type":"change-restored",
+ "change":{"project":"project-name",
+ "branch":"branch-name",
+ "topic":"topic-name",
+ "id":"Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+ "number":"123456",
+ "subject":"Commit message subject",
+ "owner":{"name":"Owner Name",
+ "email":"owner@example.com"},
+ "url":"http://review.example.com/123456"},
+ "restorer":{"name":"Restorer Name",
+ "email":"restorer@example.com"},
+ "reason":"Restore reason"}
diff --git a/testdata/comment-added-event.txt b/testdata/comment-added-event.txt
new file mode 100644
index 0000000..9ab50f7
--- /dev/null
+++ b/testdata/comment-added-event.txt
@@ -0,0 +1,25 @@
+{"type":"comment-added",
+"change":{"project":"project-name",
+ "branch":"branch-name",
+ "topic":"topic-name",
+ "id":"Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+ "number":"123456",
+ "subject":"Commit message subject",
+ "owner":{"name":"Owner Name",
+ "email":"owner@example.com"},
+ "url":"http://review.example.com/123456"},
+ "patchSet":{"number":"4",
+ "revision":"deadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+ "ref":"refs/changes/56/123456/4",
+ "uploader":{"name":"Uploader Name",
+ "email":"uploader@example.com"},
+ "createdOn":1341370514},
+ "author":{"name":"Author Name",
+ "email":"author@example.com"},
+ "approvals":[{"type":"CRVW",
+ "description":"Code Review",
+ "value":"1"},
+ {"type":"VRIF",
+ "description":"Verified",
+ "value":"1"}],
+ "comment":"Review comment"}
diff --git a/testdata/draft-published-event.txt b/testdata/draft-published-event.txt
new file mode 100644
index 0000000..8c3453a
--- /dev/null
+++ b/testdata/draft-published-event.txt
@@ -0,0 +1,18 @@
+{"type":"draft-published",
+ "change":{"project":"project-name",
+ "branch":"branch-name",
+ "topic":"topic-name",
+ "id":"Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+ "number":"123456",
+ "subject":"Commit message subject",
+ "owner":{"name":"Owner Name",
+ "email":"owner@example.com"},
+ "url":"http://review.example.com/123456"},
+ "patchSet":{"number":"4",
+ "revision":"deadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+ "ref":"refs/changes/56/123456/4",
+ "uploader":{"name":"Uploader Name",
+ "email":"uploader@example.com"},
+ "createdOn":1342075181},
+ "uploader":{"name":"Uploader Name",
+ "email":"uploader@example.com"}}
diff --git a/testdata/invalid-json.txt b/testdata/invalid-json.txt
new file mode 100644
index 0000000..f7a60bb
--- /dev/null
+++ b/testdata/invalid-json.txt
@@ -0,0 +1,4 @@
+)]}'
+{"type":"user-defined-event",
+ "title":"Event title",
+ "description":"Event description"}
diff --git a/testdata/merge-failed-event.txt b/testdata/merge-failed-event.txt
new file mode 100644
index 0000000..2d29d29
--- /dev/null
+++ b/testdata/merge-failed-event.txt
@@ -0,0 +1,19 @@
+{"type":"merge-failed",
+ "change":{"project":"project-name",
+ "branch":"branch-name",
+ "topic":"topic-name",
+ "id":"Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+ "number":"123456",
+ "subject":"Commit message subject",
+ "owner":{"name":"Owner Name",
+ "email":"owner@example.com"},
+ "url":"http://review.example.com/123456"},
+ "patchSet":{"number":"4",
+ "revision":"deadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+ "ref":"refs/changes/56/123456/4",
+ "uploader":{"name":"Uploader Name",
+ "email":"uploader@example.com"},
+ "createdOn":1341370514},
+ "submitter":{"name":"Submitter Name",
+ "email":"submitter@example.com"},
+ "reason":"Merge failed reason"}
diff --git a/testdata/patchset-created-event.txt b/testdata/patchset-created-event.txt
new file mode 100644
index 0000000..2c464be
--- /dev/null
+++ b/testdata/patchset-created-event.txt
@@ -0,0 +1,18 @@
+{"type":"patchset-created",
+ "change":{"project":"project-name",
+ "branch":"branch-name",
+ "topic":"topic-name",
+ "id":"Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+ "number":"123456",
+ "subject":"Commit message subject",
+ "owner":{"name":"Owner Name",
+ "email":"owner@example.com"},
+ "url":"http://review.example.com/123456"},
+ "patchSet":{"number":"4",
+ "revision":"deadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+ "ref":"refs/changes/56/123456/4",
+ "uploader":{"name":"Uploader Name",
+ "email":"uploader@example.com"},
+ "createdOn":1342075181},
+ "uploader":{"name":"Uploader Name",
+ "email":"uploader@example.com"}} \ No newline at end of file
diff --git a/testdata/ref-updated-event.txt b/testdata/ref-updated-event.txt
new file mode 100644
index 0000000..61944cc
--- /dev/null
+++ b/testdata/ref-updated-event.txt
@@ -0,0 +1,7 @@
+{"type":"ref-updated",
+ "submitter":{"name":"Submitter Name",
+ "email":"submitter@example.com"},
+ "refUpdate":{"oldRev":"0000000000000000000000000000000000000000",
+ "newRev":"deadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+ "refName":"refs/tags/refname",
+ "project":"project-name"}}
diff --git a/testdata/reviewer-added-event.txt b/testdata/reviewer-added-event.txt
new file mode 100644
index 0000000..b460afc
--- /dev/null
+++ b/testdata/reviewer-added-event.txt
@@ -0,0 +1,18 @@
+{"type":"reviewer-added",
+ "change":{"project":"project-name",
+ "branch":"branch-name",
+ "topic":"topic-name",
+ "id":"Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+ "number":"123456",
+ "subject":"Commit message subject",
+ "owner":{"name":"Owner Name",
+ "email":"owner@example.com"},
+ "url":"http://review.example.com/123456"},
+ "patchSet":{"number":"4",
+ "revision":"deadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+ "ref":"refs/changes/56/123456/4",
+ "uploader":{"name":"Uploader Name",
+ "email":"uploader@example.com"},
+ "createdOn":1341370514},
+ "reviewer":{"name":"Reviewer Name",
+ "email":"reviewer@example.com"}}
diff --git a/testdata/topic-changed-event.txt b/testdata/topic-changed-event.txt
new file mode 100644
index 0000000..1847440
--- /dev/null
+++ b/testdata/topic-changed-event.txt
@@ -0,0 +1,13 @@
+{"type":"topic-changed",
+ "change":{"project":"project-name",
+ "branch":"branch-name",
+ "topic":"topic-name",
+ "id":"Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+ "number":"123456",
+ "subject":"Commit message subject",
+ "owner":{"name":"Owner Name",
+ "email":"owner@example.com"},
+ "url":"http://review.example.com/123456"},
+ "changer":{"name":"Changer Name",
+ "email":"changer@example.com"},
+ "oldTopic":"old-topic"}
diff --git a/testdata/unhandled-event.txt b/testdata/unhandled-event.txt
new file mode 100644
index 0000000..6824cc8
--- /dev/null
+++ b/testdata/unhandled-event.txt
@@ -0,0 +1,3 @@
+{"type":"this-event-is-not-handled",
+ "title":"Unhandled event title",
+ "description":"Unhandled event description"}
diff --git a/testdata/user-defined-event.txt b/testdata/user-defined-event.txt
new file mode 100644
index 0000000..7f7b65e
--- /dev/null
+++ b/testdata/user-defined-event.txt
@@ -0,0 +1,3 @@
+{"type":"user-defined-event",
+ "title":"Event title",
+ "description":"Event description"}
diff --git a/unittests.py b/unittests.py
index f853e35..b4b3a23 100755
--- a/unittests.py
+++ b/unittests.py
@@ -23,10 +23,297 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
-""" Unit tests for pygerrit. """
+""" Unit tests for the Gerrit event stream handler and event objects. """
+import json
+import os
import unittest
+from pygerrit.events import PatchsetCreatedEvent, \
+ RefUpdatedEvent, ChangeMergedEvent, CommentAddedEvent, \
+ ChangeAbandonedEvent, ChangeRestoredEvent, \
+ DraftPublishedEvent, GerritEventFactory, GerritEvent, UnhandledEvent, \
+ ErrorEvent, MergeFailedEvent, ReviewerAddedEvent, TopicChangedEvent
+from pygerrit.client import GerritClient
+
+
+@GerritEventFactory.register("user-defined-event")
+class UserDefinedEvent(GerritEvent):
+
+ """ Dummy event class to test event registration. """
+
+ def __init__(self, json_data):
+ super(UserDefinedEvent, self).__init__(json_data)
+ self.title = json_data['title']
+ self.description = json_data['description']
+
+
+def _create_event(name, gerrit):
+ """ Create a new event.
+
+ Read the contents of the file specified by `name` and load as JSON
+ data, then add as an event in the `gerrit` client.
+
+ """
+ testfile = open(os.path.join("testdata", name + ".txt"))
+ data = testfile.read().replace("\n", "")
+ gerrit.put_event(data)
+ return data
+
+
+class TestGerritEvents(unittest.TestCase):
+ def setUp(self):
+ self.gerrit = GerritClient("review")
+
+ def test_patchset_created(self):
+ _create_event("patchset-created-event", self.gerrit)
+ event = self.gerrit.get_event(False)
+ self.assertTrue(isinstance(event, PatchsetCreatedEvent))
+ self.assertEquals(event.name, "patchset-created")
+ self.assertEquals(event.change.project, "project-name")
+ self.assertEquals(event.change.branch, "branch-name")
+ self.assertEquals(event.change.topic, "topic-name")
+ self.assertEquals(event.change.change_id,
+ "Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
+ self.assertEquals(event.change.number, "123456")
+ self.assertEquals(event.change.subject, "Commit message subject")
+ self.assertEquals(event.change.url, "http://review.example.com/123456")
+ self.assertEquals(event.change.owner.name, "Owner Name")
+ self.assertEquals(event.change.owner.email, "owner@example.com")
+ self.assertEquals(event.patchset.number, "4")
+ self.assertEquals(event.patchset.revision,
+ "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
+ self.assertEquals(event.patchset.ref, "refs/changes/56/123456/4")
+ self.assertEquals(event.patchset.uploader.name, "Uploader Name")
+ self.assertEquals(event.patchset.uploader.email, "uploader@example.com")
+ self.assertEquals(event.uploader.name, "Uploader Name")
+ self.assertEquals(event.uploader.email, "uploader@example.com")
+
+ def test_draft_published(self):
+ _create_event("draft-published-event", self.gerrit)
+ event = self.gerrit.get_event(False)
+ self.assertTrue(isinstance(event, DraftPublishedEvent))
+ self.assertEquals(event.name, "draft-published")
+ self.assertEquals(event.change.project, "project-name")
+ self.assertEquals(event.change.branch, "branch-name")
+ self.assertEquals(event.change.topic, "topic-name")
+ self.assertEquals(event.change.change_id,
+ "Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
+ self.assertEquals(event.change.number, "123456")
+ self.assertEquals(event.change.subject, "Commit message subject")
+ self.assertEquals(event.change.url, "http://review.example.com/123456")
+ self.assertEquals(event.change.owner.name, "Owner Name")
+ self.assertEquals(event.change.owner.email, "owner@example.com")
+ self.assertEquals(event.patchset.number, "4")
+ self.assertEquals(event.patchset.revision,
+ "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
+ self.assertEquals(event.patchset.ref, "refs/changes/56/123456/4")
+ self.assertEquals(event.patchset.uploader.name, "Uploader Name")
+ self.assertEquals(event.patchset.uploader.email, "uploader@example.com")
+ self.assertEquals(event.uploader.name, "Uploader Name")
+ self.assertEquals(event.uploader.email, "uploader@example.com")
+
+ def test_ref_updated(self):
+ _create_event("ref-updated-event", self.gerrit)
+ event = self.gerrit.get_event(False)
+ self.assertTrue(isinstance(event, RefUpdatedEvent))
+ self.assertEquals(event.name, "ref-updated")
+ self.assertEquals(event.ref_update.project, "project-name")
+ self.assertEquals(event.ref_update.oldrev,
+ "0000000000000000000000000000000000000000")
+ self.assertEquals(event.ref_update.newrev,
+ "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
+ self.assertEquals(event.ref_update.refname, "refs/tags/refname")
+ self.assertEquals(event.submitter.name, "Submitter Name")
+ self.assertEquals(event.submitter.email, "submitter@example.com")
+
+ def test_change_merged(self):
+ _create_event("change-merged-event", self.gerrit)
+ event = self.gerrit.get_event(False)
+ self.assertTrue(isinstance(event, ChangeMergedEvent))
+ self.assertEquals(event.name, "change-merged")
+ self.assertEquals(event.change.project, "project-name")
+ self.assertEquals(event.change.branch, "branch-name")
+ self.assertEquals(event.change.topic, "topic-name")
+ self.assertEquals(event.change.change_id,
+ "Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
+ self.assertEquals(event.change.number, "123456")
+ self.assertEquals(event.change.subject, "Commit message subject")
+ self.assertEquals(event.change.url, "http://review.example.com/123456")
+ self.assertEquals(event.change.owner.name, "Owner Name")
+ self.assertEquals(event.change.owner.email, "owner@example.com")
+ self.assertEquals(event.patchset.number, "4")
+ self.assertEquals(event.patchset.revision,
+ "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
+ self.assertEquals(event.patchset.ref, "refs/changes/56/123456/4")
+ self.assertEquals(event.patchset.uploader.name, "Uploader Name")
+ self.assertEquals(event.patchset.uploader.email, "uploader@example.com")
+ self.assertEquals(event.submitter.name, "Submitter Name")
+ self.assertEquals(event.submitter.email, "submitter@example.com")
+
+ def test_merge_failed(self):
+ _create_event("merge-failed-event", self.gerrit)
+ event = self.gerrit.get_event(False)
+ self.assertTrue(isinstance(event, MergeFailedEvent))
+ self.assertEquals(event.name, "merge-failed")
+ self.assertEquals(event.change.project, "project-name")
+ self.assertEquals(event.change.branch, "branch-name")
+ self.assertEquals(event.change.topic, "topic-name")
+ self.assertEquals(event.change.change_id,
+ "Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
+ self.assertEquals(event.change.number, "123456")
+ self.assertEquals(event.change.subject, "Commit message subject")
+ self.assertEquals(event.change.url, "http://review.example.com/123456")
+ self.assertEquals(event.change.owner.name, "Owner Name")
+ self.assertEquals(event.change.owner.email, "owner@example.com")
+ self.assertEquals(event.patchset.number, "4")
+ self.assertEquals(event.patchset.revision,
+ "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
+ self.assertEquals(event.patchset.ref, "refs/changes/56/123456/4")
+ self.assertEquals(event.patchset.uploader.name, "Uploader Name")
+ self.assertEquals(event.patchset.uploader.email, "uploader@example.com")
+ self.assertEquals(event.submitter.name, "Submitter Name")
+ self.assertEquals(event.submitter.email, "submitter@example.com")
+ self.assertEquals(event.reason, "Merge failed reason")
+
+ def test_comment_added(self):
+ _create_event("comment-added-event", self.gerrit)
+ event = self.gerrit.get_event(False)
+ self.assertTrue(isinstance(event, CommentAddedEvent))
+ self.assertEquals(event.name, "comment-added")
+ self.assertEquals(event.change.project, "project-name")
+ self.assertEquals(event.change.branch, "branch-name")
+ self.assertEquals(event.change.topic, "topic-name")
+ self.assertEquals(event.change.change_id,
+ "Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
+ self.assertEquals(event.change.number, "123456")
+ self.assertEquals(event.change.subject, "Commit message subject")
+ self.assertEquals(event.change.url, "http://review.example.com/123456")
+ self.assertEquals(event.change.owner.name, "Owner Name")
+ self.assertEquals(event.change.owner.email, "owner@example.com")
+ self.assertEquals(event.patchset.number, "4")
+ self.assertEquals(event.patchset.revision,
+ "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
+ self.assertEquals(event.patchset.ref, "refs/changes/56/123456/4")
+ self.assertEquals(event.patchset.uploader.name, "Uploader Name")
+ self.assertEquals(event.patchset.uploader.email, "uploader@example.com")
+ self.assertEquals(len(event.approvals), 2)
+ self.assertEquals(event.approvals[0].category, "CRVW")
+ self.assertEquals(event.approvals[0].description, "Code Review")
+ self.assertEquals(event.approvals[0].value, "1")
+ self.assertEquals(event.approvals[1].category, "VRIF")
+ self.assertEquals(event.approvals[1].description, "Verified")
+ self.assertEquals(event.approvals[1].value, "1")
+ self.assertEquals(event.author.name, "Author Name")
+ self.assertEquals(event.author.email, "author@example.com")
+
+ def test_reviewer_added(self):
+ _create_event("reviewer-added-event", self.gerrit)
+ event = self.gerrit.get_event(False)
+ self.assertTrue(isinstance(event, ReviewerAddedEvent))
+ self.assertEquals(event.name, "reviewer-added")
+ self.assertEquals(event.change.project, "project-name")
+ self.assertEquals(event.change.branch, "branch-name")
+ self.assertEquals(event.change.topic, "topic-name")
+ self.assertEquals(event.change.change_id,
+ "Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
+ self.assertEquals(event.change.number, "123456")
+ self.assertEquals(event.change.subject, "Commit message subject")
+ self.assertEquals(event.change.url, "http://review.example.com/123456")
+ self.assertEquals(event.change.owner.name, "Owner Name")
+ self.assertEquals(event.change.owner.email, "owner@example.com")
+ self.assertEquals(event.patchset.number, "4")
+ self.assertEquals(event.patchset.revision,
+ "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
+ self.assertEquals(event.patchset.ref, "refs/changes/56/123456/4")
+ self.assertEquals(event.patchset.uploader.name, "Uploader Name")
+ self.assertEquals(event.patchset.uploader.email, "uploader@example.com")
+ self.assertEquals(event.reviewer.name, "Reviewer Name")
+ self.assertEquals(event.reviewer.email, "reviewer@example.com")
+
+ def test_change_abandoned(self):
+ _create_event("change-abandoned-event", self.gerrit)
+ event = self.gerrit.get_event(False)
+ self.assertTrue(isinstance(event, ChangeAbandonedEvent))
+ self.assertEquals(event.name, "change-abandoned")
+ self.assertEquals(event.change.project, "project-name")
+ self.assertEquals(event.change.branch, "branch-name")
+ self.assertEquals(event.change.topic, "topic-name")
+ self.assertEquals(event.change.change_id,
+ "Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
+ self.assertEquals(event.change.number, "123456")
+ self.assertEquals(event.change.subject, "Commit message subject")
+ self.assertEquals(event.change.url, "http://review.example.com/123456")
+ self.assertEquals(event.change.owner.name, "Owner Name")
+ self.assertEquals(event.change.owner.email, "owner@example.com")
+ self.assertEquals(event.abandoner.name, "Abandoner Name")
+ self.assertEquals(event.abandoner.email, "abandoner@example.com")
+ self.assertEquals(event.reason, "Abandon reason")
+
+ def test_change_restored(self):
+ _create_event("change-restored-event", self.gerrit)
+ event = self.gerrit.get_event(False)
+ self.assertTrue(isinstance(event, ChangeRestoredEvent))
+ self.assertEquals(event.name, "change-restored")
+ self.assertEquals(event.change.project, "project-name")
+ self.assertEquals(event.change.branch, "branch-name")
+ self.assertEquals(event.change.topic, "topic-name")
+ self.assertEquals(event.change.change_id,
+ "Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
+ self.assertEquals(event.change.number, "123456")
+ self.assertEquals(event.change.subject, "Commit message subject")
+ self.assertEquals(event.change.url, "http://review.example.com/123456")
+ self.assertEquals(event.change.owner.name, "Owner Name")
+ self.assertEquals(event.change.owner.email, "owner@example.com")
+ self.assertEquals(event.restorer.name, "Restorer Name")
+ self.assertEquals(event.restorer.email, "restorer@example.com")
+ self.assertEquals(event.reason, "Restore reason")
+
+ def test_topic_changed(self):
+ _create_event("topic-changed-event", self.gerrit)
+ event = self.gerrit.get_event(False)
+ self.assertTrue(isinstance(event, TopicChangedEvent))
+ self.assertEquals(event.name, "topic-changed")
+ self.assertEquals(event.change.project, "project-name")
+ self.assertEquals(event.change.branch, "branch-name")
+ self.assertEquals(event.change.topic, "topic-name")
+ self.assertEquals(event.change.change_id,
+ "Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
+ self.assertEquals(event.change.number, "123456")
+ self.assertEquals(event.change.subject, "Commit message subject")
+ self.assertEquals(event.change.url, "http://review.example.com/123456")
+ self.assertEquals(event.change.owner.name, "Owner Name")
+ self.assertEquals(event.change.owner.email, "owner@example.com")
+ self.assertEquals(event.changer.name, "Changer Name")
+ self.assertEquals(event.changer.email, "changer@example.com")
+ self.assertEquals(event.oldtopic, "old-topic")
+
+ def test_user_defined_event(self):
+ _create_event("user-defined-event", self.gerrit)
+ event = self.gerrit.get_event(False)
+ self.assertTrue(isinstance(event, UserDefinedEvent))
+ self.assertEquals(event.title, "Event title")
+ self.assertEquals(event.description, "Event description")
+
+ def test_unhandled_event(self):
+ data = _create_event("unhandled-event", self.gerrit)
+ event = self.gerrit.get_event(False)
+ self.assertTrue(isinstance(event, UnhandledEvent))
+ self.assertEquals(event.json, json.loads(data))
+
+ def test_invalid_json(self):
+ _create_event("invalid-json", self.gerrit)
+ event = self.gerrit.get_event(False)
+ self.assertTrue(isinstance(event, ErrorEvent))
+
+ def test_add_duplicate_event(self):
+ try:
+ @GerritEventFactory.register("user-defined-event")
+ class AnotherUserDefinedEvent(GerritEvent):
+ pass
+ except:
+ return
+ self.fail("Did not raise exception when duplicate event registered")
if __name__ == '__main__':
unittest.main()