Pass source to project instantiations

So that we can store the canonical hostname.  Also use this to
find and store the connection name to keep the initializer signature
small.

Story: 2000953
Change-Id: Ie10f86ff3412016b411bcc511b4d9ad3af163d61
diff --git a/tests/unit/test_gerrit.py b/tests/unit/test_gerrit.py
index 999e55d..a369aff 100644
--- a/tests/unit/test_gerrit.py
+++ b/tests/unit/test_gerrit.py
@@ -22,6 +22,7 @@
 
 import tests.base
 from tests.base import BaseTestCase
+from zuul.driver.gerrit import GerritDriver
 from zuul.driver.gerrit.gerritconnection import GerritConnection
 
 FIXTURE_DIR = os.path.join(tests.base.FIXTURE_DIR, 'gerrit')
@@ -53,7 +54,8 @@
             'user': 'gerrit',
             'server': 'localhost',
         }
-        gerrit = GerritConnection(None, 'review_gerrit', gerrit_config)
+        driver = GerritDriver()
+        gerrit = GerritConnection(driver, 'review_gerrit', gerrit_config)
 
         calls, values = read_fixtures(files)
         _ssh_mock.side_effect = values
diff --git a/tests/unit/test_model.py b/tests/unit/test_model.py
index 2167a3b..7d8a058 100644
--- a/tests/unit/test_model.py
+++ b/tests/unit/test_model.py
@@ -27,19 +27,22 @@
 from tests.base import BaseTestCase, FIXTURE_DIR
 
 
-class FakeSource(object):
-    def __init__(self, name):
-        self.name = name
+class Dummy(object):
+    def __init__(self, **kw):
+        for k, v in kw.items():
+            setattr(self, k, v)
 
 
 class TestJob(BaseTestCase):
-
     def setUp(self):
         super(TestJob, self).setUp()
+        self.connection = Dummy(connection_name='dummy_connection')
+        self.source = Dummy(canonical_hostname='git.example.com',
+                            name='dummy_connection',  # TODOv3(jeblair): remove
+                            connection=self.connection)
         self.tenant = model.Tenant('tenant')
         self.layout = model.Layout()
-        self.project = model.Project('project', 'connection')
-        self.source = FakeSource('connection')
+        self.project = model.Project('project', self.source)
         self.tenant.addProjectRepo(self.source, self.project)
         self.pipeline = model.Pipeline('gate', self.layout)
         self.layout.addPipeline(self.pipeline)
@@ -162,7 +165,7 @@
         pipeline = model.Pipeline('gate', layout)
         layout.addPipeline(pipeline)
         queue = model.ChangeQueue(pipeline)
-        project = model.Project('project', None)
+        project = model.Project('project', self.source)
 
         base = configloader.JobParser.fromYaml(tenant, layout, {
             '_source_context': self.context,
@@ -508,7 +511,7 @@
         pipeline = model.Pipeline('gate', layout)
         layout.addPipeline(pipeline)
         queue = model.ChangeQueue(pipeline)
-        project = model.Project('project', None)
+        project = model.Project('project', self.source)
 
         base = configloader.JobParser.fromYaml(tenant, layout, {
             '_source_context': self.context,
@@ -554,7 +557,7 @@
     def test_job_source_project(self):
         tenant = model.Tenant('tenant')
         layout = model.Layout()
-        base_project = model.Project('base_project', None)
+        base_project = model.Project('base_project', self.source)
         base_context = model.SourceContext(base_project, 'master',
                                            'test', True)
 
@@ -565,7 +568,7 @@
         })
         layout.addJob(base)
 
-        other_project = model.Project('other_project', None)
+        other_project = model.Project('other_project', self.source)
         other_context = model.SourceContext(other_project, 'master',
                                             'test', True)
         base2 = configloader.JobParser.fromYaml(tenant, layout, {
@@ -588,7 +591,7 @@
         })
         self.layout.addJob(job)
 
-        project2 = model.Project('project2', None)
+        project2 = model.Project('project2', self.source)
         context2 = model.SourceContext(project2, 'master',
                                        'test', True)
 
diff --git a/zuul/driver/gerrit/gerritconnection.py b/zuul/driver/gerrit/gerritconnection.py
index f8d47d2..3113184 100644
--- a/zuul/driver/gerrit/gerritconnection.py
+++ b/zuul/driver/gerrit/gerritconnection.py
@@ -26,7 +26,7 @@
 import voluptuous as v
 
 from zuul.connection import BaseConnection
-from zuul.model import TriggerEvent, Project, Change, Ref
+from zuul.model import TriggerEvent, Change, Ref
 from zuul import exceptions
 
 
@@ -268,11 +268,13 @@
         self._change_cache = {}
         self.projects = {}
         self.gerrit_event_connector = None
+        self.source = driver.getSource(self)
 
     def getProject(self, name):
-        if name not in self.projects:
-            self.projects[name] = Project(name, self.connection_name)
-        return self.projects[name]
+        return self.projects.get(name)
+
+    def addProject(self, project):
+        self.projects[project.name] = project
 
     def maintainCache(self, relevant):
         # This lets the user supply a list of change objects that are
@@ -290,14 +292,14 @@
             change = self._getChange(event.change_number, event.patch_number,
                                      refresh=refresh)
         elif event.ref:
-            project = self.getProject(event.project_name)
+            project = self.source.getProject(event.project_name)
             change = Ref(project)
             change.ref = event.ref
             change.oldrev = event.oldrev
             change.newrev = event.newrev
             change.url = self._getGitwebUrl(project, sha=event.newrev)
         else:
-            project = self.getProject(event.project_name)
+            project = self.source.getProject(event.project_name)
             change = Ref(project)
             branch = event.branch or 'master'
             change.ref = 'refs/heads/%s' % branch
@@ -375,7 +377,7 @@
 
         if 'project' not in data:
             raise exceptions.ChangeNotFound(change.number, change.patchset)
-        change.project = self.getProject(data['project'])
+        change.project = self.source.getProject(data['project'])
         change.branch = data['branch']
         change.url = data['url']
         max_ps = 0
diff --git a/zuul/driver/gerrit/gerritsource.py b/zuul/driver/gerrit/gerritsource.py
index 2271cde..e6230df 100644
--- a/zuul/driver/gerrit/gerritsource.py
+++ b/zuul/driver/gerrit/gerritsource.py
@@ -14,6 +14,7 @@
 
 import logging
 from zuul.source import BaseSource
+from zuul.model import Project
 
 
 class GerritSource(BaseSource):
@@ -41,7 +42,11 @@
         return self.connection.getChange(event, refresh)
 
     def getProject(self, name):
-        return self.connection.getProject(name)
+        p = self.connection.getProject(name)
+        if not p:
+            p = Project(name, self)
+            self.connection.addProject(p)
+        return p
 
     def getProjectOpenChanges(self, project):
         return self.connection.getProjectOpenChanges(project)
diff --git a/zuul/driver/git/gitconnection.py b/zuul/driver/git/gitconnection.py
index 67f195c..ca88d3f 100644
--- a/zuul/driver/git/gitconnection.py
+++ b/zuul/driver/git/gitconnection.py
@@ -19,7 +19,6 @@
 import voluptuous as v
 
 from zuul.connection import BaseConnection
-from zuul.model import Project
 
 
 class GitConnection(BaseConnection):
@@ -44,9 +43,10 @@
         self.projects = {}
 
     def getProject(self, name):
-        if name not in self.projects:
-            self.projects[name] = Project(name, self.connection_name)
-        return self.projects[name]
+        return self.projects.get(name)
+
+    def addProject(self, project):
+        self.projects[project.name] = project
 
     def getProjectBranches(self, project):
         # TODO(jeblair): implement; this will need to handle local or
diff --git a/zuul/driver/git/gitsource.py b/zuul/driver/git/gitsource.py
index 076e8b7..485a6e4 100644
--- a/zuul/driver/git/gitsource.py
+++ b/zuul/driver/git/gitsource.py
@@ -14,6 +14,7 @@
 
 import logging
 from zuul.source import BaseSource
+from zuul.model import Project
 
 
 class GitSource(BaseSource):
@@ -38,7 +39,11 @@
         raise NotImplemented()
 
     def getProject(self, name):
-        return self.connection.getProject(name)
+        p = self.connection.getProject(name)
+        if not p:
+            p = Project(name, self)
+            self.connection.addProject(p)
+        return p
 
     def getProjectBranches(self, project):
         return self.connection.getProjectBranches(project)
diff --git a/zuul/model.py b/zuul/model.py
index d12abf4..bdc4b86 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -351,9 +351,10 @@
     # This makes a Project instance a unique identifier for a given
     # project from a given source.
 
-    def __init__(self, name, connection_name, foreign=False):
+    def __init__(self, name, source, foreign=False):
         self.name = name
-        self.connection_name = connection_name
+        self.connection_name = source.connection.connection_name
+        self.canonical_hostname = source.canonical_hostname
         # foreign projects are those referenced in dependencies
         # of layout projects, this should matter
         # when deciding whether to enqueue their changes