Merge "Add override-branch property to job repos" into feature/zuulv3
diff --git a/zuul/configloader.py b/zuul/configloader.py
index 7d03eef..6c1e388 100644
--- a/zuul/configloader.py
+++ b/zuul/configloader.py
@@ -229,6 +229,9 @@
 
         role = vs.Any(zuul_role, galaxy_role)
 
+        repo = {vs.Required('name'): str,
+                'override-branch': str}
+
         job = {vs.Required('name'): str,
                'parent': str,
                'failure-message': str,
@@ -252,7 +255,7 @@
                '_source_context': model.SourceContext,
                '_start_mark': yaml.Mark,
                'roles': to_list(role),
-               'repos': to_list(str),
+               'repos': to_list(vs.Any(repo, str)),
                'vars': dict,
                'dependencies': to_list(str),
                'allowed-projects': to_list(str),
@@ -365,9 +368,22 @@
             job.nodeset = ns
 
         if 'repos' in conf:
-            # Accumulate repos in a set so that job inheritance
-            # is additive.
-            job.repos = job.repos.union(set(conf.get('repos', [])))
+            new_repos = {}
+            repos = as_list(conf.get('repos', []))
+            for repo in repos:
+                if isinstance(repo, dict):
+                    repo_name = repo['name']
+                    repo_override_branch = repo.get('override-branch')
+                else:
+                    repo_name = repo
+                    repo_override_branch = None
+                (trusted, project) = tenant.getProject(repo_name)
+                if project is None:
+                    raise Exception("Unknown project %s" % (repo_name,))
+                job_repo = model.JobRepo(repo_name,
+                                         repo_override_branch)
+                new_repos[repo_name] = job_repo
+            job.updateRepos(new_repos)
 
         tags = conf.get('tags')
         if tags:
diff --git a/zuul/executor/client.py b/zuul/executor/client.py
index 0d40716..eb1357a 100644
--- a/zuul/executor/client.py
+++ b/zuul/executor/client.py
@@ -287,7 +287,7 @@
         params['vars']['zuul'] = zuul_params
         projects = set()
 
-        def make_project_dict(project):
+        def make_project_dict(project, override_branch=None):
             project_config = item.current_build_set.layout.project_configs.get(
                 project.canonical_name, None)
             if project_config:
@@ -297,12 +297,19 @@
             connection = project.source.connection
             return dict(connection=connection.connection_name,
                         name=project.name,
+                        override_branch=override_branch,
                         default_branch=project_default_branch)
 
         if job.repos:
-            for repo in job.repos:
-                (trusted, project) = tenant.getProject(repo)
-                params['projects'].append(make_project_dict(project))
+            for job_project in job.repos.values():
+                (trusted, project) = tenant.getProject(
+                    job_project.project_name)
+                if project is None:
+                    raise Exception("Unknown project %s" %
+                                    (job_project.project_name,))
+                params['projects'].append(
+                    make_project_dict(project,
+                                      job_project.override_branch))
                 projects.add(project)
         for item in all_items:
             if item.change.project not in projects:
diff --git a/zuul/model.py b/zuul/model.py
index ee1fede..0502168 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -752,7 +752,7 @@
             attempts=3,
             final=False,
             roles=frozenset(),
-            repos=frozenset(),
+            repos={},
             allowed_projects=None,
         )
 
@@ -820,6 +820,11 @@
         Job._deepUpdate(v, other_vars)
         self.variables = v
 
+    def updateRepos(self, other_repos):
+        repos = self.repos
+        Job._deepUpdate(repos, other_repos)
+        self.repos = repos
+
     @staticmethod
     def _deepUpdate(a, b):
         # Merge nested dictionaries if possible, otherwise, overwrite
@@ -871,7 +876,8 @@
                                     "%s=%s with variant %s" % (
                                         repr(self), k, other._get(k),
                                         repr(other)))
-                if k not in set(['pre_run', 'post_run', 'roles', 'variables']):
+                if k not in set(['pre_run', 'post_run', 'roles', 'variables',
+                                 'repos']):
                     setattr(self, k, copy.deepcopy(other._get(k)))
 
         # Don't set final above so that we don't trip an error halfway
@@ -887,6 +893,8 @@
             self.roles = self.roles.union(other.roles)
         if other._get('variables') is not None:
             self.updateVariables(other.variables)
+        if other._get('repos') is not None:
+            self.updateRepos(other.repos)
 
         for k in self.context_attributes:
             if (other._get(k) is not None and
@@ -914,6 +922,14 @@
         return True
 
 
+class JobRepo(object):
+    """ A reference to a project from a job. """
+
+    def __init__(self, project_name, override_branch=None):
+        self.project_name = project_name
+        self.override_branch = override_branch
+
+
 class JobList(object):
     """ A list of jobs in a project's pipeline. """