Fix implied branch matchers and tags

Adding an implied branch matcher to jobs on in-repo project defs
works great when an item *has* a branch.  But some items, such as
tags, don't.  With recent changes, it is now impossible for a
project to add a job in-repo that runs in a tag pipeline.

To correct this, we need to drop some of the optimizations which
assumed we could match the implied branch against existing branch
matchers, and instead, when adding a job in-repo, simply add a new
kind of branch matcher, an ImpliedBranchMatcher, that is evaluated
in a boolean 'and' with any existing branch matchers.

The ImpliedBranchMatcher only fails if the item has a branch, and
the branch doesn't match.  If the item doesn't have a branch, it
always succeeds.

This means that when a project adds a job to a tag pipeline in-repo,
it will most likely only have the ImpliedBranchMatcher, which will
simply succeed.

It also means that the multiple project configurations present in
the project's multiple branches can all add jobs to tag pipelines,
and so to remove such a job, changes may need to be made to all
branches of a project.  However, there's not much that can be done
about that at the moment.

Change-Id: Id51ddfce7ef0a6d5e3273da784e407ac72a669db
diff --git a/tests/base.py b/tests/base.py
index 036515d..f274ed6 100755
--- a/tests/base.py
+++ b/tests/base.py
@@ -486,6 +486,29 @@
         self.changes[self.change_number] = c
         return c
 
+    def addFakeTag(self, project, branch, tag):
+        path = os.path.join(self.upstream_root, project)
+        repo = git.Repo(path)
+        commit = repo.heads[branch].commit
+        newrev = commit.hexsha
+        ref = 'refs/tags/' + tag
+
+        git.Tag.create(repo, tag, commit)
+
+        event = {
+            "type": "ref-updated",
+            "submitter": {
+                "name": "User Name",
+            },
+            "refUpdate": {
+                "oldRev": 40 * '0',
+                "newRev": newrev,
+                "refName": ref,
+                "project": project,
+            }
+        }
+        return event
+
     def getFakeBranchCreatedEvent(self, project, branch):
         path = os.path.join(self.upstream_root, project)
         repo = git.Repo(path)
diff --git a/tests/fixtures/config/branch-tag/git/org_project/.zuul.yaml b/tests/fixtures/config/branch-tag/git/org_project/.zuul.yaml
new file mode 100644
index 0000000..acbba6c
--- /dev/null
+++ b/tests/fixtures/config/branch-tag/git/org_project/.zuul.yaml
@@ -0,0 +1,9 @@
+- job:
+    name: test-job
+    run: playbooks/test-job.yaml
+
+- project:
+    name: org/project
+    tag:
+      jobs:
+        - test-job
diff --git a/tests/fixtures/config/branch-tag/git/org_project/README b/tests/fixtures/config/branch-tag/git/org_project/README
new file mode 100644
index 0000000..9daeafb
--- /dev/null
+++ b/tests/fixtures/config/branch-tag/git/org_project/README
@@ -0,0 +1 @@
+test
diff --git a/tests/fixtures/config/branch-tag/git/org_project/playbooks/test-job.yaml b/tests/fixtures/config/branch-tag/git/org_project/playbooks/test-job.yaml
new file mode 100644
index 0000000..f679dce
--- /dev/null
+++ b/tests/fixtures/config/branch-tag/git/org_project/playbooks/test-job.yaml
@@ -0,0 +1,2 @@
+- hosts: all
+  tasks: []
diff --git a/tests/fixtures/config/branch-tag/git/project-config/zuul.yaml b/tests/fixtures/config/branch-tag/git/project-config/zuul.yaml
new file mode 100644
index 0000000..0ae6396
--- /dev/null
+++ b/tests/fixtures/config/branch-tag/git/project-config/zuul.yaml
@@ -0,0 +1,21 @@
+- pipeline:
+    name: tag
+    manager: independent
+    trigger:
+      gerrit:
+        - event: ref-updated
+          ref: ^refs/tags/.*$
+
+- job:
+    name: base
+    parent: null
+
+- project:
+    name: project-config
+    tag:
+      jobs: []
+
+- project:
+    name: org/project
+    tag:
+      jobs: []
diff --git a/tests/fixtures/config/branch-tag/main.yaml b/tests/fixtures/config/branch-tag/main.yaml
new file mode 100644
index 0000000..0ac232f
--- /dev/null
+++ b/tests/fixtures/config/branch-tag/main.yaml
@@ -0,0 +1,8 @@
+- tenant:
+    name: tenant-one
+    source:
+      gerrit:
+        config-projects:
+          - project-config
+        untrusted-projects:
+          - org/project
diff --git a/tests/unit/test_v3.py b/tests/unit/test_v3.py
index 70d9211..54cf111 100755
--- a/tests/unit/test_v3.py
+++ b/tests/unit/test_v3.py
@@ -157,6 +157,18 @@
         self.assertIn('Unable to modify final job', A.messages[0])
 
 
+class TestBranchTag(ZuulTestCase):
+    tenant_config_file = 'config/branch-tag/main.yaml'
+
+    def test_negative_branch_match(self):
+        # Test that a negative branch matcher works with implied branches.
+        event = self.fake_gerrit.addFakeTag('org/project', 'master', 'foo')
+        self.fake_gerrit.addEvent(event)
+        self.waitUntilSettled()
+        self.assertHistory([
+            dict(name='test-job', result='SUCCESS', ref='refs/tags/foo')])
+
+
 class TestBranchNegative(ZuulTestCase):
     tenant_config_file = 'config/branch-negative/main.yaml'