Merge "Be less agressive with implied branch matchers" into feature/zuulv3
diff --git a/tests/fixtures/config/in-repo/git/org_project1/README b/tests/fixtures/config/in-repo/git/org_project1/README
new file mode 100644
index 0000000..9daeafb
--- /dev/null
+++ b/tests/fixtures/config/in-repo/git/org_project1/README
@@ -0,0 +1 @@
+test
diff --git a/tests/fixtures/config/in-repo/main.yaml b/tests/fixtures/config/in-repo/main.yaml
index 208e274..5f57245 100644
--- a/tests/fixtures/config/in-repo/main.yaml
+++ b/tests/fixtures/config/in-repo/main.yaml
@@ -6,3 +6,4 @@
           - common-config
         untrusted-projects:
           - org/project
+          - org/project1
diff --git a/tests/unit/test_v3.py b/tests/unit/test_v3.py
index 3919418..71d94c2 100644
--- a/tests/unit/test_v3.py
+++ b/tests/unit/test_v3.py
@@ -191,6 +191,61 @@
             dict(name='project-test1', result='SUCCESS', changes='2,1'),
             dict(name='project-test2', result='SUCCESS', changes='3,1')])
 
+    def test_crd_dynamic_config_branch(self):
+        # Test that we can create a job in one repo and be able to use
+        # it from a different branch on a different repo.
+
+        self.create_branch('org/project1', 'stable')
+
+        in_repo_conf = textwrap.dedent(
+            """
+            - job:
+                name: project-test2
+
+            - project:
+                name: org/project
+                check:
+                  jobs:
+                    - project-test2
+            """)
+
+        in_repo_playbook = textwrap.dedent(
+            """
+            - hosts: all
+              tasks: []
+            """)
+
+        file_dict = {'.zuul.yaml': in_repo_conf,
+                     'playbooks/project-test2.yaml': in_repo_playbook}
+        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A',
+                                           files=file_dict)
+
+        second_repo_conf = textwrap.dedent(
+            """
+            - project:
+                name: org/project1
+                check:
+                  jobs:
+                    - project-test2
+            """)
+
+        second_file_dict = {'.zuul.yaml': second_repo_conf}
+        B = self.fake_gerrit.addFakeChange('org/project1', 'stable', 'B',
+                                           files=second_file_dict)
+        B.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
+            B.subject, A.data['id'])
+
+        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+        self.waitUntilSettled()
+        self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
+        self.waitUntilSettled()
+
+        self.assertEqual(A.reported, 1, "A should report")
+        self.assertHistory([
+            dict(name='project-test2', result='SUCCESS', changes='1,1'),
+            dict(name='project-test2', result='SUCCESS', changes='1,1 2,1'),
+        ])
+
     def test_untrusted_syntax_error(self):
         in_repo_conf = textwrap.dedent(
             """
diff --git a/zuul/configloader.py b/zuul/configloader.py
index d981d8c..4da761c 100644
--- a/zuul/configloader.py
+++ b/zuul/configloader.py
@@ -271,7 +271,29 @@
     ]
 
     @staticmethod
-    def fromYaml(tenant, layout, conf):
+    def _getImpliedBranches(reference, job, project_pipeline):
+        # If the current job definition is not in the same branch as
+        # the reference definition of this job, and this is a project
+        # repo, add an implicit branch matcher for this branch
+        # (assuming there are no explicit branch matchers).  But only
+        # for top-level job definitions and variants.
+        # Project-pipeline job variants should more closely attach to
+        # their branch if they appear in a project-repo.
+        if (reference and
+            reference.source_context and
+            reference.source_context.branch != job.source_context.branch):
+            same_context = False
+        else:
+            same_context = True
+
+        if (job.source_context and
+            (not job.source_context.trusted) and
+            ((not same_context) or project_pipeline)):
+            return [job.source_context.branch]
+        return None
+
+    @staticmethod
+    def fromYaml(tenant, layout, conf, project_pipeline=False):
         with configuration_exceptions('job', conf):
             JobParser.getSchema()(conf)
 
@@ -280,6 +302,8 @@
         # them (e.g., "job.run = ..." rather than
         # "job.run.append(...)").
 
+        reference = layout.jobs.get(conf['name'], [None])[0]
+
         job = model.Job(conf['name'])
         job.source_context = conf.get('_source_context')
         if 'auth' in conf:
@@ -316,9 +340,10 @@
             run = model.PlaybookContext(job.source_context, run_name)
             job.run = (run,)
         else:
-            run_name = os.path.join('playbooks', job.name)
-            run = model.PlaybookContext(job.source_context, run_name)
-            job.implied_run = (run,) + job.implied_run
+            if not project_pipeline:
+                run_name = os.path.join('playbooks', job.name)
+                run = model.PlaybookContext(job.source_context, run_name)
+                job.implied_run = (run,) + job.implied_run
 
         for k in JobParser.simple_attributes:
             a = k.replace('-', '_')
@@ -350,13 +375,14 @@
 
         job.dependencies = frozenset(as_list(conf.get('dependencies')))
 
-        roles = []
-        for role in conf.get('roles', []):
-            if 'zuul' in role:
-                r = JobParser._makeZuulRole(tenant, job, role)
-                if r:
-                    roles.append(r)
-        job.roles = job.roles.union(set(roles))
+        if 'roles' in conf:
+            roles = []
+            for role in conf.get('roles', []):
+                if 'zuul' in role:
+                    r = JobParser._makeZuulRole(tenant, job, role)
+                    if r:
+                        roles.append(r)
+            job.roles = job.roles.union(set(roles))
 
         variables = conf.get('vars', None)
         if variables:
@@ -372,14 +398,20 @@
                 allowed.append(project.name)
             job.allowed_projects = frozenset(allowed)
 
-        # If the definition for this job came from a project repo,
-        # implicitly apply a branch matcher for the branch it was on.
-        if (not job.source_context.trusted):
-            branches = [job.source_context.branch]
-        elif 'branches' in conf:
+        # If the current job definition is not in the same branch as
+        # the reference definition of this job, and this is a project
+        # repo, add an implicit branch matcher for this branch
+        # (assuming there are no explicit branch matchers).  But only
+        # for top-level job definitions and variants.
+        # Project-pipeline job variants should more closely attach to
+        # their branch if they appear in a project-repo.
+
+        branches = None
+        if (project_pipeline or 'branches' not in conf):
+            branches = JobParser._getImpliedBranches(
+                reference, job, project_pipeline)
+        if (not branches) and ('branches' in conf):
             branches = as_list(conf['branches'])
-        else:
-            branches = None
         if branches:
             matchers = []
             for branch in branches:
@@ -456,23 +488,22 @@
                       start_mark, job_list):
         for conf_job in conf:
             if isinstance(conf_job, six.string_types):
-                job = model.Job(conf_job)
-                job_list.addJob(job)
+                attrs = dict(name=conf_job)
             elif isinstance(conf_job, dict):
                 # A dictionary in a job tree may override params
                 jobname, attrs = conf_job.items()[0]
                 if attrs:
                     # We are overriding params, so make a new job def
                     attrs['name'] = jobname
-                    attrs['_source_context'] = source_context
-                    attrs['_start_mark'] = start_mark
-                    job_list.addJob(JobParser.fromYaml(tenant, layout, attrs))
                 else:
                     # Not overriding, so add a blank job
-                    job = model.Job(jobname)
-                    job_list.addJob(job)
+                    attrs = dict(name=jobname)
             else:
                 raise Exception("Job must be a string or dictionary")
+            attrs['_source_context'] = source_context
+            attrs['_start_mark'] = start_mark
+            job_list.addJob(JobParser.fromYaml(tenant, layout, attrs,
+                                               project_pipeline=True))
 
 
 class ProjectParser(object):