Optionally limit github to protected branches

When using a branch and pull model on a shared repository there are
usually one or more protected branches which are gated and a dynamic
number of temporary personal/feature branches which are the source for
the pull requests. These temporary branches while ungated can
potentially include broken zuul config and therefore break the global
tenant wide configuration.

In order to deal with this model add support for excluding unprotected
branches. This can be configured on tenant level and overridden per
project.

Change-Id: I8a45fd41539a3c964a84142f04c1644585c0fdcf
diff --git a/tests/base.py b/tests/base.py
index eca31fe..0f188bd 100755
--- a/tests/base.py
+++ b/tests/base.py
@@ -567,7 +567,10 @@
         def __init__(self):
             self._branches = [FakeGithub.FakeBranch()]
 
-        def branches(self):
+        def branches(self, protected=False):
+            if protected:
+                # simulate there is no protected branch
+                return []
             return self._branches
 
     def user(self, login):
diff --git a/tests/fixtures/config/tenant-parser/unprotected-branches.yaml b/tests/fixtures/config/tenant-parser/unprotected-branches.yaml
new file mode 100644
index 0000000..bf2feef
--- /dev/null
+++ b/tests/fixtures/config/tenant-parser/unprotected-branches.yaml
@@ -0,0 +1,11 @@
+- tenant:
+    name: tenant-one
+    exclude-unprotected-branches: true
+    source:
+      gerrit:
+        config-projects:
+          - common-config:
+              exclude-unprotected-branches: false
+        untrusted-projects:
+          - org/project1
+          - org/project2
diff --git a/tests/fixtures/config/unprotected-branches/git/org_common-config/zuul.yaml b/tests/fixtures/config/unprotected-branches/git/org_common-config/zuul.yaml
new file mode 100644
index 0000000..c0fbf0d
--- /dev/null
+++ b/tests/fixtures/config/unprotected-branches/git/org_common-config/zuul.yaml
@@ -0,0 +1,19 @@
+- pipeline:
+    name: check
+    manager: independent
+    trigger:
+      github:
+        - event: pull_request
+          action:
+            - opened
+            - changed
+            - reopened
+    success:
+      github:
+        status: 'success'
+    failure:
+      github:
+        status: 'failure'
+    start:
+      github:
+        comment: true
diff --git a/tests/fixtures/config/unprotected-branches/git/org_project1/README b/tests/fixtures/config/unprotected-branches/git/org_project1/README
new file mode 100644
index 0000000..9daeafb
--- /dev/null
+++ b/tests/fixtures/config/unprotected-branches/git/org_project1/README
@@ -0,0 +1 @@
+test
diff --git a/tests/fixtures/config/unprotected-branches/git/org_project1/playbooks/project-test.yaml b/tests/fixtures/config/unprotected-branches/git/org_project1/playbooks/project-test.yaml
new file mode 100644
index 0000000..f679dce
--- /dev/null
+++ b/tests/fixtures/config/unprotected-branches/git/org_project1/playbooks/project-test.yaml
@@ -0,0 +1,2 @@
+- hosts: all
+  tasks: []
diff --git a/tests/fixtures/config/unprotected-branches/git/org_project1/zuul.yaml b/tests/fixtures/config/unprotected-branches/git/org_project1/zuul.yaml
new file mode 100644
index 0000000..31abadf
--- /dev/null
+++ b/tests/fixtures/config/unprotected-branches/git/org_project1/zuul.yaml
@@ -0,0 +1,8 @@
+- job:
+    name: project-test
+
+- project:
+    name: org/project1
+    check:
+      jobs:
+        - project-test
diff --git a/tests/fixtures/config/unprotected-branches/git/org_project2/README b/tests/fixtures/config/unprotected-branches/git/org_project2/README
new file mode 100644
index 0000000..9daeafb
--- /dev/null
+++ b/tests/fixtures/config/unprotected-branches/git/org_project2/README
@@ -0,0 +1 @@
+test
diff --git a/tests/fixtures/config/unprotected-branches/git/org_project2/zuul.yaml b/tests/fixtures/config/unprotected-branches/git/org_project2/zuul.yaml
new file mode 100644
index 0000000..64d316d
--- /dev/null
+++ b/tests/fixtures/config/unprotected-branches/git/org_project2/zuul.yaml
@@ -0,0 +1 @@
+This zuul.yaml is intentionally broken and should not be loaded on startup.
diff --git a/tests/fixtures/config/unprotected-branches/main.yaml b/tests/fixtures/config/unprotected-branches/main.yaml
new file mode 100644
index 0000000..8078d37
--- /dev/null
+++ b/tests/fixtures/config/unprotected-branches/main.yaml
@@ -0,0 +1,10 @@
+- tenant:
+    name: tenant-one
+    source:
+      github:
+        config-projects:
+          - org/common-config
+        untrusted-projects:
+          - org/project1
+          - org/project2:
+              exclude-unprotected-branches: true
diff --git a/tests/unit/test_configloader.py b/tests/unit/test_configloader.py
index d08c6a1..1ba4ed9 100644
--- a/tests/unit/test_configloader.py
+++ b/tests/unit/test_configloader.py
@@ -182,6 +182,7 @@
 
     def test_tenant_groups3(self):
         tenant = self.sched.abide.tenants.get('tenant-one')
+        self.assertEqual(False, tenant.exclude_unprotected_branches)
         self.assertEqual(['common-config'],
                          [x.name for x in tenant.config_projects])
         self.assertEqual(['org/project1', 'org/project2'],
@@ -212,6 +213,29 @@
                         project2_config.pipelines['check'].job_list.jobs)
 
 
+class TestTenantUnprotectedBranches(TenantParserTestCase):
+    tenant_config_file = 'config/tenant-parser/unprotected-branches.yaml'
+
+    def test_tenant_unprotected_branches(self):
+        tenant = self.sched.abide.tenants.get('tenant-one')
+        self.assertEqual(True, tenant.exclude_unprotected_branches)
+
+        self.assertEqual(['common-config'],
+                         [x.name for x in tenant.config_projects])
+        self.assertEqual(['org/project1', 'org/project2'],
+                         [x.name for x in tenant.untrusted_projects])
+
+        tpc = tenant.project_configs
+        project_name = tenant.config_projects[0].canonical_name
+        self.assertEqual(False, tpc[project_name].exclude_unprotected_branches)
+
+        project_name = tenant.untrusted_projects[0].canonical_name
+        self.assertIsNone(tpc[project_name].exclude_unprotected_branches)
+
+        project_name = tenant.untrusted_projects[1].canonical_name
+        self.assertIsNone(tpc[project_name].exclude_unprotected_branches)
+
+
 class TestSplitConfig(ZuulTestCase):
     tenant_config_file = 'config/split-config/main.yaml'
 
diff --git a/tests/unit/test_github_driver.py b/tests/unit/test_github_driver.py
index 0e199df..68fbe29 100644
--- a/tests/unit/test_github_driver.py
+++ b/tests/unit/test_github_driver.py
@@ -683,3 +683,20 @@
             self.fake_github.emitEvent,
             ('ping', pevent),
         )
+
+
+class TestGithubUnprotectedBranches(ZuulTestCase):
+    config_file = 'zuul-github-driver.conf'
+    tenant_config_file = 'config/unprotected-branches/main.yaml'
+
+    def test_unprotected_branches(self):
+        tenant = self.sched.abide.tenants.get('tenant-one')
+
+        project1 = tenant.untrusted_projects[0]
+        project2 = tenant.untrusted_projects[1]
+
+        # project1 should have parsed master
+        self.assertIn('master', project1.unparsed_branch_config.keys())
+
+        # project2 should have no parsed branch
+        self.assertEqual(0, len(project2.unparsed_branch_config.keys()))