Merge "Add a project index to Tenant" into feature/zuulv3
diff --git a/tests/unit/test_model.py b/tests/unit/test_model.py
index 7d8a058..7269473 100644
--- a/tests/unit/test_model.py
+++ b/tests/unit/test_model.py
@@ -781,3 +781,137 @@
         graph.addJob(jobs[3])
         jobs[6].dependencies = frozenset([jobs[2].name])
         graph.addJob(jobs[6])
+
+
+class TestTenant(BaseTestCase):
+    def test_add_project(self):
+        tenant = model.Tenant('tenant')
+        connection1 = Dummy(connection_name='dummy_connection1')
+        source1 = Dummy(canonical_hostname='git1.example.com',
+                        name='dummy',  # TODOv3(jeblair): remove
+                        connection=connection1)
+
+        source1_project1 = model.Project('project1', source1)
+        tenant.addConfigRepo(source1, source1_project1)
+        d = {'project1':
+             {'git1.example.com': source1_project1}}
+        self.assertEqual(d, tenant.projects)
+        self.assertEqual((True, source1_project1),
+                         tenant.getProject('project1'))
+        self.assertEqual((True, source1_project1),
+                         tenant.getProject('git1.example.com/project1'))
+
+        source1_project2 = model.Project('project2', source1)
+        tenant.addProjectRepo(source1, source1_project2)
+        d = {'project1':
+             {'git1.example.com': source1_project1},
+             'project2':
+             {'git1.example.com': source1_project2}}
+        self.assertEqual(d, tenant.projects)
+        self.assertEqual((False, source1_project2),
+                         tenant.getProject('project2'))
+        self.assertEqual((False, source1_project2),
+                         tenant.getProject('git1.example.com/project2'))
+
+        connection2 = Dummy(connection_name='dummy_connection2')
+        source2 = Dummy(canonical_hostname='git2.example.com',
+                        name='dummy',  # TODOv3(jeblair): remove
+                        connection=connection2)
+
+        source2_project1 = model.Project('project1', source2)
+        tenant.addProjectRepo(source2, source2_project1)
+        d = {'project1':
+             {'git1.example.com': source1_project1,
+              'git2.example.com': source2_project1},
+             'project2':
+             {'git1.example.com': source1_project2}}
+        self.assertEqual(d, tenant.projects)
+        with testtools.ExpectedException(
+                Exception,
+                "Project name 'project1' is ambiguous"):
+            tenant.getProject('project1')
+        self.assertEqual((False, source1_project2),
+                         tenant.getProject('project2'))
+        self.assertEqual((True, source1_project1),
+                         tenant.getProject('git1.example.com/project1'))
+        self.assertEqual((False, source2_project1),
+                         tenant.getProject('git2.example.com/project1'))
+
+        source2_project2 = model.Project('project2', source2)
+        tenant.addConfigRepo(source2, source2_project2)
+        d = {'project1':
+             {'git1.example.com': source1_project1,
+              'git2.example.com': source2_project1},
+             'project2':
+             {'git1.example.com': source1_project2,
+              'git2.example.com': source2_project2}}
+        self.assertEqual(d, tenant.projects)
+        with testtools.ExpectedException(
+                Exception,
+                "Project name 'project1' is ambiguous"):
+            tenant.getProject('project1')
+        with testtools.ExpectedException(
+                Exception,
+                "Project name 'project2' is ambiguous"):
+            tenant.getProject('project2')
+        self.assertEqual((True, source1_project1),
+                         tenant.getProject('git1.example.com/project1'))
+        self.assertEqual((False, source2_project1),
+                         tenant.getProject('git2.example.com/project1'))
+        self.assertEqual((False, source1_project2),
+                         tenant.getProject('git1.example.com/project2'))
+        self.assertEqual((True, source2_project2),
+                         tenant.getProject('git2.example.com/project2'))
+
+        source1_project2b = model.Project('subpath/project2', source1)
+        tenant.addConfigRepo(source1, source1_project2b)
+        d = {'project1':
+             {'git1.example.com': source1_project1,
+              'git2.example.com': source2_project1},
+             'project2':
+             {'git1.example.com': source1_project2,
+              'git2.example.com': source2_project2},
+             'subpath/project2':
+             {'git1.example.com': source1_project2b}}
+        self.assertEqual(d, tenant.projects)
+        self.assertEqual((False, source1_project2),
+                         tenant.getProject('git1.example.com/project2'))
+        self.assertEqual((True, source2_project2),
+                         tenant.getProject('git2.example.com/project2'))
+        self.assertEqual((True, source1_project2b),
+                         tenant.getProject('subpath/project2'))
+        self.assertEqual(
+            (True, source1_project2b),
+            tenant.getProject('git1.example.com/subpath/project2'))
+
+        source2_project2b = model.Project('subpath/project2', source2)
+        tenant.addConfigRepo(source2, source2_project2b)
+        d = {'project1':
+             {'git1.example.com': source1_project1,
+              'git2.example.com': source2_project1},
+             'project2':
+             {'git1.example.com': source1_project2,
+              'git2.example.com': source2_project2},
+             'subpath/project2':
+             {'git1.example.com': source1_project2b,
+              'git2.example.com': source2_project2b}}
+        self.assertEqual(d, tenant.projects)
+        self.assertEqual((False, source1_project2),
+                         tenant.getProject('git1.example.com/project2'))
+        self.assertEqual((True, source2_project2),
+                         tenant.getProject('git2.example.com/project2'))
+        with testtools.ExpectedException(
+                Exception,
+                "Project name 'subpath/project2' is ambiguous"):
+            tenant.getProject('subpath/project2')
+        self.assertEqual(
+            (True, source1_project2b),
+            tenant.getProject('git1.example.com/subpath/project2'))
+        self.assertEqual(
+            (True, source2_project2b),
+            tenant.getProject('git2.example.com/subpath/project2'))
+
+        with testtools.ExpectedException(
+                Exception,
+                "Project project1 is already in project index"):
+            tenant._addProject(source1_project1)
diff --git a/zuul/configloader.py b/zuul/configloader.py
index fd56fab..6b8c4f6 100644
--- a/zuul/configloader.py
+++ b/zuul/configloader.py
@@ -774,12 +774,12 @@
         tenant = model.Tenant(conf['name'])
         tenant.unparsed_config = conf
         unparsed_config = model.UnparsedTenantConfig()
-        tenant.config_repos, tenant.project_repos = \
+        config_repos, project_repos = \
             TenantParser._loadTenantConfigRepos(
                 project_key_dir, connections, conf)
-        for source, repo in tenant.config_repos:
+        for source, repo in config_repos:
             tenant.addConfigRepo(source, repo)
-        for source, repo in tenant.project_repos:
+        for source, repo in project_repos:
             tenant.addProjectRepo(source, repo)
         tenant.config_repos_config, tenant.project_repos_config = \
             TenantParser._loadTenantInRepoLayouts(merger, connections,
diff --git a/zuul/model.py b/zuul/model.py
index bdc4b86..fc3c63e 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -355,6 +355,7 @@
         self.name = name
         self.connection_name = source.connection.connection_name
         self.canonical_hostname = source.canonical_hostname
+        self.canonical_name = source.canonical_hostname + '/' + name
         # foreign projects are those referenced in dependencies
         # of layout projects, this should matter
         # when deciding whether to enqueue their changes
@@ -2504,29 +2505,103 @@
         # The list of repos from which we will read main
         # configuration.  (source, project)
         self.config_repos = []
+        # TODOv3(jeblair): This will replace the above list but drops the
+        # source element of the tuple.
+        self._config_repos = set()
         # The unparsed config from those repos.
         self.config_repos_config = None
         # The list of projects from which we will read in-repo
         # configuration.  (source, project)
         self.project_repos = []
+        # TODOv3(jeblair): This will replace the above list but drops the
+        # source element of the tuple.
+        self._project_repos = set()
         # The unparsed config from those repos.
         self.project_repos_config = None
         # A mapping of source -> {config_repos: {}, project_repos: {}}
         self.sources = {}
-
         self.semaphore_handler = SemaphoreHandler()
 
+        # A mapping of project names to projects.  project_name ->
+        # VALUE where VALUE is a further dictionary of
+        # canonical_hostname -> Project.
+        self.projects = {}
+        self.canonical_hostnames = set()
+
+    def _addProject(self, project):
+        """Add a project to the project index
+
+        :arg Project project: The project to add.
+        """
+        self.canonical_hostnames.add(project.canonical_hostname)
+        hostname_dict = self.projects.setdefault(project.name, {})
+        if project.canonical_hostname in hostname_dict:
+            raise Exception("Project %s is already in project index" %
+                            (project,))
+        hostname_dict[project.canonical_hostname] = project
+
+    def getProject(self, name):
+        """Return a project given its name.
+
+        :arg str name: The name of the project.  It may be fully
+            qualified (E.g., "git.example.com/subpath/project") or may
+            contain only the project name name may be supplied (E.g.,
+            "subpath/project").
+
+        :returns: A tuple (trusted, project) or (None, None) if the
+            project is not found or ambiguous.  The "trusted" boolean
+            indicates whether or not the project is trusted by this
+            tenant.
+        :rtype: (bool, Project)
+
+        """
+        path = name.split('/', 1)
+        if path[0] in self.canonical_hostnames:
+            hostname = path[0]
+            project_name = path[1]
+        else:
+            hostname = None
+            project_name = name
+        hostname_dict = self.projects.get(project_name)
+        project = None
+        if hostname_dict:
+            if hostname:
+                project = hostname_dict.get(hostname)
+            else:
+                values = hostname_dict.values()
+                if len(values) == 1:
+                    project = values[0]
+                else:
+                    raise Exception("Project name '%s' is ambiguous, "
+                                    "please fully qualify the project "
+                                    "with a hostname" % (name,))
+        if project is None:
+            return (None, None)
+        if project in self._config_repos:
+            return (True, project)
+        if project in self._project_repos:
+            return (False, project)
+        # This should never happen:
+        raise Exception("Project %s is neither trusted nor untrusted" %
+                        (project,))
+
     def addConfigRepo(self, source, project):
         sd = self.sources.setdefault(source.name,
                                      {'config_repos': {},
                                       'project_repos': {}})
         sd['config_repos'][project.name] = project
+        self.config_repos.append((source, project))
+        self._config_repos.add(project)
+        self._addProject(project)
 
     def addProjectRepo(self, source, project):
         sd = self.sources.setdefault(source.name,
                                      {'config_repos': {},
                                       'project_repos': {}})
         sd['project_repos'][project.name] = project
+        self.project_repos.append((source, project))
+        self._project_repos.add(project)
+        self._addProject(project)
 
     def getRepo(self, source, project_name):
         """Get a project given a source and project name