summaryrefslogtreecommitdiff
path: root/pygerrit
diff options
context:
space:
mode:
Diffstat (limited to 'pygerrit')
-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
8 files changed, 910 insertions, 0 deletions
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()