Merge "Don't wait for forever to join streamer" into feature/zuulv3
diff --git a/tests/base.py b/tests/base.py
index 76f7e03..7d33ffc 100755
--- a/tests/base.py
+++ b/tests/base.py
@@ -776,21 +776,6 @@
         repo = self._getRepo()
         return repo.references[self._getPRReference()].commit.hexsha
 
-    def setStatus(self, sha, state, url, description, context, user='zuul'):
-        # Since we're bypassing github API, which would require a user, we
-        # hard set the user as 'zuul' here.
-        # insert the status at the top of the list, to simulate that it
-        # is the most recent set status
-        self.statuses[sha].insert(0, ({
-            'state': state,
-            'url': url,
-            'description': description,
-            'context': context,
-            'creator': {
-                'login': user
-            }
-        }))
-
     def addReview(self, user, state, granted_on=None):
         gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
         # convert the timestamp to a str format that would be returned
@@ -882,6 +867,7 @@
         self.connection_name = connection_name
         self.pr_number = 0
         self.pull_requests = []
+        self.statuses = {}
         self.upstream_root = upstream_root
         self.merge_failure = False
         self.merge_not_allowed_count = 0
@@ -949,7 +935,8 @@
                 'repo': {
                     'full_name': pr.project
                 }
-            }
+            },
+            'files': pr.files
         }
         return data
 
@@ -960,10 +947,6 @@
         pr = prs[0]
         return self.getPull(pr.project, pr.number)
 
-    def getPullFileNames(self, project, number):
-        pr = self.pull_requests[number - 1]
-        return pr.files
-
     def _getPullReviews(self, owner, project, number):
         pr = self.pull_requests[number - 1]
         return pr.reviews
@@ -1014,25 +997,24 @@
         pull_request.merge_message = commit_message
 
     def getCommitStatuses(self, project, sha):
-        owner, proj = project.split('/')
-        for pr in self.pull_requests:
-            pr_owner, pr_project = pr.project.split('/')
-            # This is somewhat risky, if the same commit exists in multiple
-            # PRs, we might grab the wrong one that doesn't have a status
-            # that is expected to be there. Maybe re-work this so that there
-            # is a global registry of commit statuses like with github.
-            if (pr_owner == owner and pr_project == proj and
-                sha in pr.statuses):
-                return pr.statuses[sha]
+        return self.statuses.get(project, {}).get(sha, [])
 
-    def setCommitStatus(self, project, sha, state,
-                        url='', description='', context=''):
-        owner, proj = project.split('/')
-        for pr in self.pull_requests:
-            pr_owner, pr_project = pr.project.split('/')
-            if (pr_owner == owner and pr_project == proj and
-                pr.head_sha == sha):
-                pr.setStatus(sha, state, url, description, context)
+    def setCommitStatus(self, project, sha, state, url='', description='',
+                        context='default', user='zuul'):
+        # always insert a status to the front of the list, to represent
+        # the last status provided for a commit.
+        # Since we're bypassing github API, which would require a user, we
+        # default the user as 'zuul' here.
+        self.statuses.setdefault(project, {}).setdefault(sha, [])
+        self.statuses[project][sha].insert(0, {
+            'state': state,
+            'url': url,
+            'description': description,
+            'context': context,
+            'creator': {
+                'login': user
+            }
+        })
 
     def labelPull(self, project, pr_number, label):
         pull_request = self.pull_requests[pr_number - 1]
diff --git a/tests/unit/test_github_driver.py b/tests/unit/test_github_driver.py
index 4979087..ba8e497 100644
--- a/tests/unit/test_github_driver.py
+++ b/tests/unit/test_github_driver.py
@@ -248,16 +248,18 @@
 
     @simple_layout('layouts/reporting-github.yaml', driver='github')
     def test_reporting(self):
+        project = 'org/project'
         # pipeline reports pull status both on start and success
         self.executor_server.hold_jobs_in_build = True
-        A = self.fake_github.openFakePullRequest('org/project', 'master', 'A')
+        A = self.fake_github.openFakePullRequest(project, 'master', 'A')
         self.fake_github.emitEvent(A.getPullRequestOpenedEvent())
         self.waitUntilSettled()
         # We should have a status container for the head sha
-        self.assertIn(A.head_sha, A.statuses.keys())
+        statuses = self.fake_github.statuses[project][A.head_sha]
+        self.assertIn(A.head_sha, self.fake_github.statuses[project].keys())
         # We should only have one status for the head sha
-        self.assertEqual(1, len(A.statuses[A.head_sha]))
-        check_status = A.statuses[A.head_sha][0]
+        self.assertEqual(1, len(statuses))
+        check_status = statuses[0]
         check_url = ('http://zuul.example.com/status/#%s,%s' %
                      (A.number, A.head_sha))
         self.assertEqual('tenant-one/check', check_status['context'])
@@ -270,8 +272,9 @@
         self.executor_server.release()
         self.waitUntilSettled()
         # We should only have two statuses for the head sha
-        self.assertEqual(2, len(A.statuses[A.head_sha]))
-        check_status = A.statuses[A.head_sha][0]
+        statuses = self.fake_github.statuses[project][A.head_sha]
+        self.assertEqual(2, len(statuses))
+        check_status = statuses[0]
         check_url = ('http://zuul.example.com/status/#%s,%s' %
                      (A.number, A.head_sha))
         self.assertEqual('tenant-one/check', check_status['context'])
@@ -286,7 +289,8 @@
         self.fake_github.emitEvent(
             A.getCommentAddedEvent('reporting check'))
         self.waitUntilSettled()
-        self.assertEqual(2, len(A.statuses[A.head_sha]))
+        statuses = self.fake_github.statuses[project][A.head_sha]
+        self.assertEqual(2, len(statuses))
         # comments increased by one for the start message
         self.assertEqual(2, len(A.comments))
         self.assertThat(A.comments[1],
@@ -295,8 +299,9 @@
         self.executor_server.release()
         self.waitUntilSettled()
         # pipeline reports success status
-        self.assertEqual(3, len(A.statuses[A.head_sha]))
-        report_status = A.statuses[A.head_sha][0]
+        statuses = self.fake_github.statuses[project][A.head_sha]
+        self.assertEqual(3, len(statuses))
+        report_status = statuses[0]
         self.assertEqual('tenant-one/reporting', report_status['context'])
         self.assertEqual('success', report_status['state'])
         self.assertEqual(2, len(A.comments))
diff --git a/tests/unit/test_github_requirements.py b/tests/unit/test_github_requirements.py
index 5dd6e80..301ea2f 100644
--- a/tests/unit/test_github_requirements.py
+++ b/tests/unit/test_github_requirements.py
@@ -25,7 +25,8 @@
     @simple_layout('layouts/requirements-github.yaml', driver='github')
     def test_pipeline_require_status(self):
         "Test pipeline requirement: status"
-        A = self.fake_github.openFakePullRequest('org/project1', 'master', 'A')
+        project = 'org/project1'
+        A = self.fake_github.openFakePullRequest(project, 'master', 'A')
         # A comment event that we will keep submitting to trigger
         comment = A.getCommentAddedEvent('test me')
         self.fake_github.emitEvent(comment)
@@ -34,13 +35,15 @@
         self.assertEqual(len(self.history), 0)
 
         # An error status should not cause it to be enqueued
-        A.setStatus(A.head_sha, 'error', 'null', 'null', 'check')
+        self.fake_github.setCommitStatus(project, A.head_sha, 'error',
+                                         context='check')
         self.fake_github.emitEvent(comment)
         self.waitUntilSettled()
         self.assertEqual(len(self.history), 0)
 
         # A success status goes in
-        A.setStatus(A.head_sha, 'success', 'null', 'null', 'check')
+        self.fake_github.setCommitStatus(project, A.head_sha, 'success',
+                                         context='check')
         self.fake_github.emitEvent(comment)
         self.waitUntilSettled()
         self.assertEqual(len(self.history), 1)
@@ -49,7 +52,8 @@
     @simple_layout('layouts/requirements-github.yaml', driver='github')
     def test_trigger_require_status(self):
         "Test trigger requirement: status"
-        A = self.fake_github.openFakePullRequest('org/project1', 'master', 'A')
+        project = 'org/project1'
+        A = self.fake_github.openFakePullRequest(project, 'master', 'A')
         # A comment event that we will keep submitting to trigger
         comment = A.getCommentAddedEvent('trigger me')
         self.fake_github.emitEvent(comment)
@@ -58,13 +62,15 @@
         self.assertEqual(len(self.history), 0)
 
         # An error status should not cause it to be enqueued
-        A.setStatus(A.head_sha, 'error', 'null', 'null', 'check')
+        self.fake_github.setCommitStatus(project, A.head_sha, 'error',
+                                         context='check')
         self.fake_github.emitEvent(comment)
         self.waitUntilSettled()
         self.assertEqual(len(self.history), 0)
 
         # A success status goes in
-        A.setStatus(A.head_sha, 'success', 'null', 'null', 'check')
+        self.fake_github.setCommitStatus(project, A.head_sha, 'success',
+                                         context='check')
         self.fake_github.emitEvent(comment)
         self.waitUntilSettled()
         self.assertEqual(len(self.history), 1)
@@ -73,10 +79,12 @@
     @simple_layout('layouts/requirements-github.yaml', driver='github')
     def test_trigger_on_status(self):
         "Test trigger on: status"
-        A = self.fake_github.openFakePullRequest('org/project2', 'master', 'A')
+        project = 'org/project2'
+        A = self.fake_github.openFakePullRequest(project, 'master', 'A')
 
         # An error status should not cause it to be enqueued
-        A.setStatus(A.head_sha, 'error', 'null', 'null', 'check')
+        self.fake_github.setCommitStatus(project, A.head_sha, 'error',
+                                         context='check')
         self.fake_github.emitEvent(A.getCommitStatusEvent('check',
                                                           state='error'))
         self.waitUntilSettled()
@@ -84,7 +92,8 @@
 
         # A success status from unknown user should not cause it to be
         # enqueued
-        A.setStatus(A.head_sha, 'success', 'null', 'null', 'check', user='foo')
+        self.fake_github.setCommitStatus(project, A.head_sha, 'success',
+                                         context='check', user='foo')
         self.fake_github.emitEvent(A.getCommitStatusEvent('check',
                                                           state='success',
                                                           user='foo'))
@@ -92,7 +101,8 @@
         self.assertEqual(len(self.history), 0)
 
         # A success status from zuul goes in
-        A.setStatus(A.head_sha, 'success', 'null', 'null', 'check')
+        self.fake_github.setCommitStatus(project, A.head_sha, 'success',
+                                         context='check')
         self.fake_github.emitEvent(A.getCommitStatusEvent('check'))
         self.waitUntilSettled()
         self.assertEqual(len(self.history), 1)
@@ -100,7 +110,8 @@
 
         # An error status for a different context should not cause it to be
         # enqueued
-        A.setStatus(A.head_sha, 'error', 'null', 'null', 'gate')
+        self.fake_github.setCommitStatus(project, A.head_sha, 'error',
+                                         context='gate')
         self.fake_github.emitEvent(A.getCommitStatusEvent('gate',
                                                           state='error'))
         self.waitUntilSettled()
diff --git a/tests/unit/test_scheduler.py b/tests/unit/test_scheduler.py
index d9e20be..839007d 100755
--- a/tests/unit/test_scheduler.py
+++ b/tests/unit/test_scheduler.py
@@ -1516,7 +1516,7 @@
         trusted, project = tenant.getProject('org/project')
         url = self.fake_gerrit.getGitUrl(project)
         self.executor_server.merger._addProject('review.example.com',
-                                                'org/project', url)
+                                                'org/project', url, None)
         A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
         A.addPatchset(large=True)
         # TODOv3(jeblair): add hostname to upstream root
diff --git a/zuul/ansible/library/zuul_console.py b/zuul/ansible/library/zuul_console.py
index 7f8a1b6..42f41f0 100644
--- a/zuul/ansible/library/zuul_console.py
+++ b/zuul/ansible/library/zuul_console.py
@@ -15,10 +15,12 @@
 # You should have received a copy of the GNU General Public License
 # along with this software.  If not, see <http://www.gnu.org/licenses/>.
 
+import glob
 import os
 import sys
 import select
 import socket
+import subprocess
 import threading
 import time
 
@@ -196,6 +198,53 @@
                 pass
 
 
+def get_inode(port_number=19885):
+    for netfile in ('/proc/net/tcp6', '/proc/net/tcp'):
+        if not os.path.exists(netfile):
+            continue
+        with open(netfile) as f:
+            # discard header line
+            f.readline()
+            for line in f:
+                # sl local_address rem_address st tx_queue:rx_queue tr:tm->when
+                # retrnsmt   uid  timeout inode
+                fields = line.split()
+                # Format is localaddr:localport in hex
+                port = int(fields[1].split(':')[1], base=16)
+                if port == port_number:
+                    return fields[9]
+
+
+def get_pid_from_inode(inode):
+    my_euid = os.geteuid()
+    exceptions = []
+    for d in os.listdir('/proc'):
+        try:
+            try:
+                int(d)
+            except Exception as e:
+                continue
+            d_abs_path = os.path.join('/proc', d)
+            if os.stat(d_abs_path).st_uid != my_euid:
+                continue
+            fd_dir = os.path.join(d_abs_path, 'fd')
+            if os.path.exists(fd_dir):
+                if os.stat(fd_dir).st_uid != my_euid:
+                    continue
+                for fd in os.listdir(fd_dir):
+                    try:
+                        fd_path = os.path.join(fd_dir, fd)
+                        if os.path.islink(fd_path):
+                            target = os.readlink(fd_path)
+                            if '[' + inode + ']' in target:
+                                return d, exceptions
+                    except Exception as e:
+                        exceptions.append(e)
+        except Exception as e:
+            exceptions.append(e)
+    return None, exceptions
+
+
 def test():
     s = Server(LOG_STREAM_FILE, LOG_STREAM_PORT)
     s.run()
@@ -206,19 +255,54 @@
         argument_spec=dict(
             path=dict(default=LOG_STREAM_FILE),
             port=dict(default=LOG_STREAM_PORT, type='int'),
+            state=dict(default='present', choices=['absent', 'present']),
         )
     )
 
     p = module.params
     path = p['path']
     port = p['port']
+    state = p['state']
 
-    if daemonize():
+    if state == 'present':
+        if daemonize():
+            module.exit_json()
+
+        s = Server(path, port)
+        s.run()
+    else:
+        pid = None
+        exceptions = []
+        inode = get_inode()
+        if not inode:
+            module.fail_json(
+                "Could not find inode for port",
+                exceptions=[])
+
+        pid, exceptions = get_pid_from_inode(inode)
+        if not pid:
+            except_strings = [str(e) for e in exceptions]
+            module.fail_json(
+                msg="Could not find zuul_console process for inode",
+                exceptions=except_strings)
+
+        try:
+            subprocess.check_output(['kill', pid])
+        except subprocess.CalledProcessError as e:
+            module.fail_json(
+                msg="Could not kill zuul_console pid",
+                exceptions=[str(e)])
+
+        for fn in glob.glob(LOG_STREAM_FILE.format(log_uuid='*')):
+            try:
+                os.unlink(fn)
+            except Exception as e:
+                module.fail_json(
+                    msg="Could not remove logfile {fn}".format(fn=fn),
+                    exceptions=[str(e)])
+
         module.exit_json()
 
-    s = Server(path, port)
-    s.run()
-
 from ansible.module_utils.basic import *  # noqa
 from ansible.module_utils.basic import AnsibleModule
 
diff --git a/zuul/configloader.py b/zuul/configloader.py
index 029b840..5e0fe65 100644
--- a/zuul/configloader.py
+++ b/zuul/configloader.py
@@ -327,6 +327,7 @@
                'dependencies': to_list(str),
                'allowed-projects': to_list(str),
                'override-branch': str,
+               'description': str,
                }
 
         return vs.Schema(job)
diff --git a/zuul/driver/github/githubconnection.py b/zuul/driver/github/githubconnection.py
index 659d88b..4910e51 100644
--- a/zuul/driver/github/githubconnection.py
+++ b/zuul/driver/github/githubconnection.py
@@ -118,6 +118,12 @@
             event = None
 
         if event:
+            if event.change_number:
+                project = self.connection.source.getProject(event.project_name)
+                self.connection._getChange(project,
+                                           event.change_number,
+                                           event.patch_number,
+                                           refresh=True)
             event.project_hostname = self.connection.canonical_hostname
             self.connection.logEvent(event)
             self.connection.sched.addEvent(event)
@@ -463,28 +469,20 @@
             if change not in relevant:
                 del self._change_cache[key]
 
-    def getChange(self, event):
+    def getChange(self, event, refresh=False):
         """Get the change representing an event."""
 
         project = self.source.getProject(event.project_name)
         if event.change_number:
-            change = PullRequest(event.project_name)
-            change.project = project
-            change.number = event.change_number
+            change = self._getChange(project, event.change_number,
+                                     event.patch_number, refresh=refresh)
             change.refspec = event.refspec
             change.branch = event.branch
             change.url = event.change_url
             change.updated_at = self._ghTimestampToDate(event.updated_at)
-            change.patchset = event.patch_number
-            change.files = self.getPullFileNames(project, change.number)
-            change.title = event.title
-            change.status = self._get_statuses(project, event.patch_number)
-            change.reviews = self.getPullReviews(project, change.number)
             change.source_event = event
-            change.open = self.getPullOpen(event.project_name, change.number)
-            change.is_current_patchset = self.getIsCurrent(event.project_name,
-                                                           change.number,
-                                                           event.patch_number)
+            change.is_current_patchset = (change.pr.get('head').get('sha') ==
+                                          event.patch_number)
         elif event.ref:
             change = Ref(project)
             change.ref = event.ref
@@ -497,6 +495,38 @@
             change = Ref(project)
         return change
 
+    def _getChange(self, project, number, patchset, refresh=False):
+        key = '%s/%s/%s' % (project.name, number, patchset)
+        change = self._change_cache.get(key)
+        if change and not refresh:
+            return change
+        if not change:
+            change = PullRequest(project.name)
+            change.project = project
+            change.number = number
+            change.patchset = patchset
+        self._change_cache[key] = change
+        try:
+            self._updateChange(change)
+        except Exception:
+            if key in self._change_cache:
+                del self._change_cache[key]
+            raise
+        return change
+
+    def _updateChange(self, change):
+        self.log.info("Updating %s" % (change,))
+        change.pr = self.getPull(change.project.name, change.number)
+        change.files = change.pr.get('files')
+        change.title = change.pr.get('title')
+        change.open = change.pr.get('state') == 'open'
+        change.status = self._get_statuses(change.project,
+                                           change.patchset)
+        change.reviews = self.getPullReviews(change.project,
+                                             change.number)
+
+        return change
+
     def getGitUrl(self, project):
         if self.git_ssh_key:
             return 'ssh://git@%s/%s.git' % (self.git_host, project)
@@ -535,7 +565,9 @@
     def getPull(self, project_name, number):
         github = self.getGithubClient(project_name)
         owner, proj = project_name.split('/')
-        pr = github.pull_request(owner, proj, number).as_dict()
+        probj = github.pull_request(owner, proj, number)
+        pr = probj.as_dict()
+        pr['files'] = [f.filename for f in probj.files()]
         log_rate_limit(self.log, github)
         return pr
 
@@ -578,14 +610,6 @@
             return None
         return pulls.pop()
 
-    def getPullFileNames(self, project, number):
-        github = self.getGithubClient(project)
-        owner, proj = project.name.split('/')
-        filenames = [f.filename for f in
-                     github.pull_request(owner, proj, number).files()]
-        log_rate_limit(self.log, github)
-        return filenames
-
     def getPullReviews(self, project, number):
         owner, proj = project.name.split('/')
 
@@ -723,14 +747,6 @@
         pull_request.remove_label(label)
         log_rate_limit(self.log, github)
 
-    def getPullOpen(self, project, number):
-        pr = self.getPull(project, number)
-        return pr.get('state') == 'open'
-
-    def getIsCurrent(self, project, number, sha):
-        pr = self.getPull(project, number)
-        return pr.get('head').get('sha') == sha
-
     def getPushedFileNames(self, event):
         files = set()
         for c in event.commits:
diff --git a/zuul/driver/github/githubsource.py b/zuul/driver/github/githubsource.py
index 1350b10..1c2f727 100644
--- a/zuul/driver/github/githubsource.py
+++ b/zuul/driver/github/githubsource.py
@@ -58,8 +58,8 @@
         """Called after configuration has been processed."""
         pass
 
-    def getChange(self, event):
-        return self.connection.getChange(event)
+    def getChange(self, event, refresh=False):
+        return self.connection.getChange(event, refresh)
 
     def getProject(self, name):
         p = self.connection.getProject(name)
@@ -87,10 +87,6 @@
         """Get the git-web url for a project."""
         return self.connection.getGitwebUrl(project, sha)
 
-    def getPullFiles(self, project, number):
-        """Get filenames of the pull request"""
-        return self.connection.getPullFileNames(project, number)
-
     def _ghTimestampToDate(self, timestamp):
         return time.strptime(timestamp, '%Y-%m-%dT%H:%M:%SZ')
 
diff --git a/zuul/executor/server.py b/zuul/executor/server.py
index f44fd50..c498fa4 100644
--- a/zuul/executor/server.py
+++ b/zuul/executor/server.py
@@ -363,6 +363,7 @@
         self.hostname = socket.gethostname()
         self.zuul_url = config.get('merger', 'zuul_url')
         self.merger_lock = threading.Lock()
+        self.verbose = False
         self.command_map = dict(
             stop=self.stop,
             pause=self.pause,
@@ -522,12 +523,10 @@
         pass
 
     def verboseOn(self):
-        # TODOv3: implement
-        pass
+        self.verbose = True
 
     def verboseOff(self):
-        # TODOv3: implement
-        pass
+        self.verbose = False
 
     def join(self):
         self.update_thread.join()
@@ -1308,7 +1307,7 @@
         env_copy = os.environ.copy()
         env_copy['LOGNAME'] = 'zuul'
 
-        if False:  # TODOv3: self.options['verbose']:
+        if self.executor_server.verbose:
             verbose = '-vvv'
         else:
             verbose = '-v'
diff --git a/zuul/lib/log_streamer.py b/zuul/lib/log_streamer.py
index 59d5240..6695723 100644
--- a/zuul/lib/log_streamer.py
+++ b/zuul/lib/log_streamer.py
@@ -144,6 +144,11 @@
                 else:
                     break
 
+            # See if the file has been removed, meaning we should stop
+            # streaming it.
+            if not os.path.exists(log.path):
+                return False
+
             # At this point, we are waiting for more data to be written
             time.sleep(0.5)
 
@@ -159,16 +164,6 @@
                 if not ret:
                     return False
 
-            # See if the file has been truncated
-            try:
-                st = os.stat(log.path)
-                if (st.st_ino != log.stat.st_ino or
-                    st.st_size < log.size):
-                    return True
-            except Exception:
-                return True
-            log.size = st.st_size
-
 
 class CustomForkingTCPServer(ss.ForkingTCPServer):
     '''
diff --git a/zuul/merger/merger.py b/zuul/merger/merger.py
index 6cfd904..2ac0de8 100644
--- a/zuul/merger/merger.py
+++ b/zuul/merger/merger.py
@@ -42,12 +42,16 @@
 
 
 class Repo(object):
-    def __init__(self, remote, local, email, username,
+    def __init__(self, remote, local, email, username, sshkey=None,
                  cache_path=None, logger=None):
         if logger is None:
             self.log = logging.getLogger("zuul.Repo")
         else:
             self.log = logger
+        if sshkey:
+            self.env = {'GIT_SSH_COMMAND': 'ssh -i %s' % (sshkey,)}
+        else:
+            self.env = {}
         self.remote_url = remote
         self.local_path = local
         self.email = email
@@ -69,11 +73,14 @@
             self.log.debug("Cloning from %s to %s" % (self.remote_url,
                                                       self.local_path))
             if self.cache_path:
-                git.Repo.clone_from(self.cache_path, self.local_path)
+                git.Repo.clone_from(self.cache_path, self.local_path,
+                                    env=self.env)
                 rewrite_url = True
             else:
-                git.Repo.clone_from(self.remote_url, self.local_path)
+                git.Repo.clone_from(self.remote_url, self.local_path,
+                                    env=self.env)
         repo = git.Repo(self.local_path)
+        repo.git.update_environment(**self.env)
         # Create local branches corresponding to all the remote branches
         if not repo_is_cloned:
             origin = repo.remotes.origin
@@ -98,6 +105,7 @@
     def createRepoObject(self):
         self._ensure_cloned()
         repo = git.Repo(self.local_path)
+        repo.git.update_environment(**self.env)
         return repo
 
     def reset(self):
@@ -282,23 +290,7 @@
         self.username = username
         self.cache_root = cache_root
 
-    def _get_ssh_cmd(self, connection_name):
-        sshkey = self.connections.connections.get(connection_name).\
-            connection_config.get('sshkey')
-        if sshkey:
-            return 'ssh -i %s' % sshkey
-        else:
-            return None
-
-    def _setGitSsh(self, connection_name):
-        wrapper_name = '.ssh_wrapper_%s' % connection_name
-        name = os.path.join(self.working_root, wrapper_name)
-        if os.path.isfile(name):
-            os.environ['GIT_SSH'] = name
-        elif 'GIT_SSH' in os.environ:
-            del os.environ['GIT_SSH']
-
-    def _addProject(self, hostname, project_name, url):
+    def _addProject(self, hostname, project_name, url, sshkey):
         repo = None
         key = '/'.join([hostname, project_name])
         try:
@@ -308,8 +300,8 @@
                                           project_name)
             else:
                 cache_path = None
-            repo = Repo(url, path, self.email, self.username, cache_path,
-                        self.logger)
+            repo = Repo(url, path, self.email, self.username,
+                        sshkey, cache_path, self.logger)
 
             self.repos[key] = repo
         except Exception:
@@ -325,11 +317,13 @@
         key = '/'.join([hostname, project_name])
         if key in self.repos:
             return self.repos[key]
+        sshkey = self.connections.connections.get(connection_name).\
+            connection_config.get('sshkey')
         if not url:
             raise Exception("Unable to set up repo for project %s/%s"
                             " without a url" %
                             (connection_name, project_name,))
-        return self._addProject(hostname, project_name, url)
+        return self._addProject(hostname, project_name, url, sshkey)
 
     def updateRepo(self, connection_name, project_name):
         # TODOv3(jhesketh): Reimplement
@@ -437,28 +431,26 @@
         else:
             self.log.debug("Found base commit %s for %s" % (base, key,))
         # Merge the change
-        with repo.createRepoObject().git.custom_environment(
-            GIT_SSH_COMMAND=self._get_ssh_cmd(item['connection'])):
-            commit = self._mergeChange(item, base)
-            if not commit:
+        commit = self._mergeChange(item, base)
+        if not commit:
+            return None
+        # Store this commit as the most recent for this project-branch
+        recent[key] = commit
+        # Set the Zuul ref for this item to point to the most recent
+        # commits of each project-branch
+        for key, mrc in recent.items():
+            connection, project, branch = key
+            zuul_ref = None
+            try:
+                repo = self.getRepo(connection, project)
+                zuul_ref = branch + '/' + item['ref']
+                if not repo.getCommitFromRef(zuul_ref):
+                    repo.createZuulRef(zuul_ref, mrc)
+            except Exception:
+                self.log.exception("Unable to set zuul ref %s for "
+                                   "item %s" % (zuul_ref, item))
                 return None
-            # Store this commit as the most recent for this project-branch
-            recent[key] = commit
-            # Set the Zuul ref for this item to point to the most recent
-            # commits of each project-branch
-            for key, mrc in recent.items():
-                connection, project, branch = key
-                zuul_ref = None
-                try:
-                    repo = self.getRepo(connection, project)
-                    zuul_ref = branch + '/' + item['ref']
-                    if not repo.getCommitFromRef(zuul_ref):
-                        repo.createZuulRef(zuul_ref, mrc)
-                except Exception:
-                    self.log.exception("Unable to set zuul ref %s for "
-                                       "item %s" % (zuul_ref, item))
-                    return None
-            return commit
+        return commit
 
     def mergeChanges(self, items, files=None, repo_state=None):
         # connection+project+branch -> commit