Merge "Enable test_new_patchset_check / test_abandoned_check" into feature/zuulv3
diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py
index c98fe29..0295494 100755
--- a/tests/test_scheduler.py
+++ b/tests/test_scheduler.py
@@ -4608,17 +4608,24 @@
                  pipeline='dup1'),
             dict(name='project-test1', result='SUCCESS', changes='1,1',
                  pipeline='dup2'),
-        ])
+        ], ordered=False)
 
         self.assertEqual(len(A.messages), 2)
 
-        self.assertIn('dup1', A.messages[0])
-        self.assertNotIn('dup2', A.messages[0])
-        self.assertIn('project-test1', A.messages[0])
-
-        self.assertIn('dup2', A.messages[1])
-        self.assertNotIn('dup1', A.messages[1])
-        self.assertIn('project-test1', A.messages[1])
+        if 'dup1' in A.messages[0]:
+            self.assertIn('dup1', A.messages[0])
+            self.assertNotIn('dup2', A.messages[0])
+            self.assertIn('project-test1', A.messages[0])
+            self.assertIn('dup2', A.messages[1])
+            self.assertNotIn('dup1', A.messages[1])
+            self.assertIn('project-test1', A.messages[1])
+        else:
+            self.assertIn('dup1', A.messages[1])
+            self.assertNotIn('dup2', A.messages[1])
+            self.assertIn('project-test1', A.messages[1])
+            self.assertIn('dup2', A.messages[0])
+            self.assertNotIn('dup1', A.messages[0])
+            self.assertIn('project-test1', A.messages[0])
 
 
 class TestSchedulerOneJobProject(ZuulTestCase):
diff --git a/zuul/configloader.py b/zuul/configloader.py
index 3f723fb..9d0afa5 100644
--- a/zuul/configloader.py
+++ b/zuul/configloader.py
@@ -103,6 +103,7 @@
                'nodes': vs.Any([node], str),
                'timeout': int,
                '_source_project': model.Project,
+               '_source_branch': vs.Any(str, None),
                }
 
         return vs.Schema(job)
@@ -142,18 +143,28 @@
             # accumulate onto any previously applied tags from
             # metajobs.
             job.tags = job.tags.union(set(tags))
-        # This attribute may not be overridden -- it is always
-        # supplied by the config loader and is the Project instance of
-        # the repo where it originated.
+        # The source attributes may not be overridden -- they are
+        # always supplied by the config loader.  They correspond to
+        # the Project instance of the repo where it originated, and
+        # the branch name.
         job.source_project = conf.get('_source_project')
+        job.source_branch = conf.get('_source_branch')
         job.failure_message = conf.get('failure-message', job.failure_message)
         job.success_message = conf.get('success-message', job.success_message)
         job.failure_url = conf.get('failure-url', job.failure_url)
         job.success_url = conf.get('success-url', job.success_url)
 
-        if 'branches' in conf:
+        # If the definition for this job came from a project repo,
+        # implicitly apply a branch matcher for the branch it was on.
+        if job.source_branch:
+            branches = [job.source_branch]
+        elif 'branches' in conf:
+            branches = as_list(conf['branches'])
+        else:
+            branches = None
+        if branches:
             matchers = []
-            for branch in as_list(conf['branches']):
+            for branch in branches:
                 matchers.append(change_matcher.BranchMatcher(branch))
             job.branch_matcher = change_matcher.MatchAny(matchers)
         if 'files' in conf:
@@ -238,6 +249,8 @@
 
     @staticmethod
     def fromYaml(layout, conf):
+        # TODOv3(jeblair): This may need some branch-specific
+        # configuration for in-repo configs.
         ProjectParser.getSchema(layout)(conf)
         conf_templates = conf.pop('templates', [])
         # The way we construct a project definition is by parsing the
@@ -551,12 +564,17 @@
             # Get in-project-repo config files which have a restricted
             # set of options.
             url = source.getGitUrl(project)
-            # TODOv3(jeblair): config should be branch specific
-            job = merger.getFiles(project.name, url, 'master',
-                                  files=['.zuul.yaml'])
-            job.project = project
-            job.config_repo = False
-            jobs.append(job)
+            # For each branch in the repo, get the zuul.yaml for that
+            # branch.  Remember the branch and then implicitly add a
+            # branch selector to each job there.  This makes the
+            # in-repo configuration apply only to that branch.
+            for branch in source.getProjectBranches(project):
+                job = merger.getFiles(project.name, url, branch,
+                                      files=['.zuul.yaml'])
+                job.project = project
+                job.branch = branch
+                job.config_repo = False
+                jobs.append(job)
 
         for job in jobs:
             # Note: this is an ordered list -- we wait for cat jobs to
@@ -576,7 +594,7 @@
                         config_repos_config.extend(incdata)
                     else:
                         incdata = TenantParser._parseProjectRepoLayout(
-                            job.files[fn], job.project)
+                            job.files[fn], job.project, job.branch)
                         project_repos_config.extend(incdata)
                     job.project.unparsed_config = incdata
         return config_repos_config, project_repos_config
@@ -590,11 +608,11 @@
         return config
 
     @staticmethod
-    def _parseProjectRepoLayout(data, project):
+    def _parseProjectRepoLayout(data, project, branch):
         # TODOv3(jeblair): this should implement some rules to protect
         # aspects of the config that should not be changed in-repo
         config = model.UnparsedTenantConfig()
-        config.extend(yaml.load(data), project)
+        config.extend(yaml.load(data), project, branch)
 
         return config
 
@@ -659,13 +677,15 @@
         config = tenant.config_repos_config.copy()
         for source, project in tenant.project_repos:
             # TODOv3(jeblair): config should be branch specific
-            data = files.getFile(project.name, 'master', '.zuul.yaml')
-            if not data:
-                data = project.unparsed_config
-            if not data:
-                continue
-            incdata = TenantParser._parseProjectRepoLayout(data, project)
-            config.extend(incdata)
+            for branch in source.getProjectBranches(project):
+                data = files.getFile(project.name, branch, '.zuul.yaml')
+                if not data:
+                    data = project.unparsed_config
+                if not data:
+                    continue
+                incdata = TenantParser._parseProjectRepoLayout(
+                    data, project, branch)
+                config.extend(incdata)
 
         layout = model.Layout()
         # TODOv3(jeblair): copying the pipelines could be dangerous/confusing.
diff --git a/zuul/connection/gerrit.py b/zuul/connection/gerrit.py
index 4ebdd56..f084993 100644
--- a/zuul/connection/gerrit.py
+++ b/zuul/connection/gerrit.py
@@ -571,6 +571,12 @@
                                    (record.get('number'),))
         return changes
 
+    def getProjectBranches(self, project):
+        refs = self.getInfoRefs(project)
+        heads = [str(k[len('refs/heads/'):]) for k in refs.keys()
+                 if k.startswith('refs/heads/')]
+        return heads
+
     def addEvent(self, data):
         return self.event_queue.put((time.time(), data))
 
diff --git a/zuul/model.py b/zuul/model.py
index 53c4646..77b1259 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -1640,7 +1640,7 @@
         r.nodesets = copy.deepcopy(self.nodesets)
         return r
 
-    def extend(self, conf, source_project=None):
+    def extend(self, conf, source_project=None, source_branch=None):
         if isinstance(conf, UnparsedTenantConfig):
             self.pipelines.extend(conf.pipelines)
             self.jobs.extend(conf.jobs)
@@ -1668,6 +1668,8 @@
             elif key == 'job':
                 if source_project is not None:
                     value['_source_project'] = source_project
+                if source_branch is not None:
+                    value['_source_branch'] = source_branch
                 self.jobs.append(value)
             elif key == 'project-template':
                 self.project_templates.append(value)
diff --git a/zuul/source/__init__.py b/zuul/source/__init__.py
index d92d47a..69dc162 100644
--- a/zuul/source/__init__.py
+++ b/zuul/source/__init__.py
@@ -63,3 +63,7 @@
     @abc.abstractmethod
     def getProject(self, name):
         """Get a project."""
+
+    @abc.abstractmethod
+    def getProjectBranches(self, project):
+        """Get branches for a project"""
diff --git a/zuul/source/gerrit.py b/zuul/source/gerrit.py
index 85227c7..8b03135 100644
--- a/zuul/source/gerrit.py
+++ b/zuul/source/gerrit.py
@@ -41,6 +41,9 @@
     def getProjectOpenChanges(self, project):
         return self.connection.getProjectOpenChanges(project)
 
+    def getProjectBranches(self, project):
+        return self.connection.getProjectBranches(project)
+
     def getGitUrl(self, project):
         return self.connection.getGitUrl(project)