Refactor out Changeish

This makes everything a child of Ref. As a result of this rearrangement,
NullChanges are awkward to use, and thus, have been replaced by enqueued
Refs where the 'ref' attribute is still unknown. As a result,
status.json will show timer-created jobs with an ID, where they used to
show a null, which is why that change to test_timer is necessary.

Change-Id: Ief0d3dde089b5529b9df7a804f6fea72b8b7dc48
Story: 2000781
Task: 3300
diff --git a/tests/unit/test_scheduler.py b/tests/unit/test_scheduler.py
index ec88737..8c5ef06 100755
--- a/tests/unit/test_scheduler.py
+++ b/tests/unit/test_scheduler.py
@@ -2806,7 +2806,6 @@
             for q in p['change_queues']:
                 for head in q['heads']:
                     for change in head:
-                        self.assertEqual(change['id'], None)
                         for job in change['jobs']:
                             status_jobs.add(job['name'])
         self.assertIn('project-bitrot-stable-old', status_jobs)
diff --git a/zuul/change_matcher.py b/zuul/change_matcher.py
index 845ba1c..1da1d2c 100644
--- a/zuul/change_matcher.py
+++ b/zuul/change_matcher.py
@@ -62,7 +62,8 @@
     def matches(self, change):
         return (
             (hasattr(change, 'branch') and self.regex.match(change.branch)) or
-            (hasattr(change, 'ref') and self.regex.match(change.ref))
+            (hasattr(change, 'ref') and
+             change.ref is not None and self.regex.match(change.ref))
         )
 
 
diff --git a/zuul/driver/gerrit/gerritconnection.py b/zuul/driver/gerrit/gerritconnection.py
index 286006f..829c175 100644
--- a/zuul/driver/gerrit/gerritconnection.py
+++ b/zuul/driver/gerrit/gerritconnection.py
@@ -26,7 +26,7 @@
 import voluptuous as v
 
 from zuul.connection import BaseConnection
-from zuul.model import TriggerEvent, Project, Change, Ref, NullChange
+from zuul.model import TriggerEvent, Project, Change, Ref
 from zuul import exceptions
 
 
@@ -292,7 +292,13 @@
             change.url = self._getGitwebUrl(project, sha=event.newrev)
         else:
             project = self.getProject(event.project_name)
-            change = NullChange(project)
+            change = Ref(project)
+            branch = event.branch or 'master'
+            change.ref = 'refs/heads/%s' % branch
+            refs = self.getInfoRefs(project)
+            change.oldrev = refs[change.ref]
+            change.newrev = refs[change.ref]
+            change.url = self._getGitwebUrl(project, sha=change.newrev)
         return change
 
     def _getChange(self, number, patchset, refresh=False, history=None):
diff --git a/zuul/driver/zuul/__init__.py b/zuul/driver/zuul/__init__.py
index 1bc0ee9..47ccec0 100644
--- a/zuul/driver/zuul/__init__.py
+++ b/zuul/driver/zuul/__init__.py
@@ -87,7 +87,7 @@
     def _createParentChangeEnqueuedEvents(self, change, pipeline):
         self.log.debug("Checking for changes needing %s:" % change)
         if not hasattr(change, 'needed_by_changes'):
-            self.log.debug("  Changeish does not support dependencies")
+            self.log.debug("  %s does not support dependencies" % type(change))
             return
         for needs in change.needed_by_changes:
             self._createParentChangeEnqueuedEvent(needs, pipeline)
diff --git a/zuul/executor/client.py b/zuul/executor/client.py
index 31646f8..476c238 100644
--- a/zuul/executor/client.py
+++ b/zuul/executor/client.py
@@ -258,7 +258,7 @@
             params['ZUUL_CHANGE_IDS'] = zuul_changes
             params['ZUUL_CHANGE'] = str(item.change.number)
             params['ZUUL_PATCHSET'] = str(item.change.patchset)
-        if hasattr(item.change, 'ref'):
+        if hasattr(item.change, 'ref') and item.change.ref is not None:
             params['ZUUL_REFNAME'] = item.change.ref
             params['ZUUL_OLDREV'] = item.change.oldrev
             params['ZUUL_NEWREV'] = item.change.newrev
diff --git a/zuul/manager/dependent.py b/zuul/manager/dependent.py
index f5fa579..4c48568 100644
--- a/zuul/manager/dependent.py
+++ b/zuul/manager/dependent.py
@@ -89,7 +89,7 @@
         to_enqueue = []
         self.log.debug("Checking for changes needing %s:" % change)
         if not hasattr(change, 'needed_by_changes'):
-            self.log.debug("  Changeish does not support dependencies")
+            self.log.debug("  %s does not support dependencies" % type(change))
             return
         for other_change in change.needed_by_changes:
             with self.getChangeQueue(other_change) as other_change_queue:
@@ -133,7 +133,7 @@
         # Return true if okay to proceed enqueing this change,
         # false if the change should not be enqueued.
         if not hasattr(change, 'needs_changes'):
-            self.log.debug("  Changeish does not support dependencies")
+            self.log.debug("  %s does not support dependencies" % type(change))
             return True
         if not change.needs_changes:
             self.log.debug("  No changes needed")
diff --git a/zuul/manager/independent.py b/zuul/manager/independent.py
index 3d28327..9e2a7d6 100644
--- a/zuul/manager/independent.py
+++ b/zuul/manager/independent.py
@@ -62,7 +62,7 @@
         # Return true if okay to proceed enqueing this change,
         # false if the change should not be enqueued.
         if not hasattr(change, 'needs_changes'):
-            self.log.debug("  Changeish does not support dependencies")
+            self.log.debug("  %s does not support dependencies" % type(change))
             return True
         if not change.needs_changes:
             self.log.debug("  No changes needed")
diff --git a/zuul/model.py b/zuul/model.py
index 3676b68..4663a5a 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -1578,27 +1578,49 @@
         return ret
 
 
-class Changeish(object):
-    """Base class for Change and Ref."""
+class Ref(object):
+    """An existing state of a Project."""
 
     def __init__(self, project):
         self.project = project
+        self.ref = None
+        self.oldrev = None
+        self.newrev = None
 
     def getBasePath(self):
         base_path = ''
-        if hasattr(self, 'refspec'):
-            base_path = "%s/%s/%s" % (
-                self.number[-2:], self.number, self.patchset)
-        elif hasattr(self, 'ref'):
+        if hasattr(self, 'ref'):
             base_path = "%s/%s" % (self.newrev[:2], self.newrev)
 
         return base_path
 
+    def _id(self):
+        return self.newrev
+
+    def __repr__(self):
+        rep = None
+        if self.newrev == '0000000000000000000000000000000000000000':
+            rep = '<Ref 0x%x deletes %s from %s' % (
+                  id(self), self.ref, self.oldrev)
+        elif self.oldrev == '0000000000000000000000000000000000000000':
+            rep = '<Ref 0x%x creates %s on %s>' % (
+                  id(self), self.ref, self.newrev)
+        else:
+            # Catch all
+            rep = '<Ref 0x%x %s updated %s..%s>' % (
+                  id(self), self.ref, self.oldrev, self.newrev)
+
+        return rep
+
     def equals(self, other):
-        raise NotImplementedError()
+        if (self.project == other.project
+            and self.ref == other.ref
+            and self.newrev == other.newrev):
+            return True
+        return False
 
     def isUpdateOf(self, other):
-        raise NotImplementedError()
+        return False
 
     def filterJobs(self, jobs):
         return filter(lambda job: job.changeMatches(self), jobs)
@@ -1610,7 +1632,7 @@
         return False
 
 
-class Change(Changeish):
+class Change(Ref):
     """A proposed new state for a Project."""
     def __init__(self, project):
         super(Change, self).__init__(project)
@@ -1638,6 +1660,12 @@
     def __repr__(self):
         return '<Change 0x%x %s>' % (id(self), self._id())
 
+    def getBasePath(self):
+        if hasattr(self, 'refspec'):
+            return "%s/%s/%s" % (
+                self.number[-2:], self.number, self.patchset)
+        return super(Change, self).getBasePath()
+
     def equals(self, other):
         if self.number == other.number and self.patchset == other.patchset:
             return True
@@ -1667,44 +1695,7 @@
         return False
 
 
-class Ref(Changeish):
-    """An existing state of a Project."""
-    def __init__(self, project):
-        super(Ref, self).__init__(project)
-        self.ref = None
-        self.oldrev = None
-        self.newrev = None
-
-    def _id(self):
-        return self.newrev
-
-    def __repr__(self):
-        rep = None
-        if self.newrev == '0000000000000000000000000000000000000000':
-            rep = '<Ref 0x%x deletes %s from %s' % (
-                  id(self), self.ref, self.oldrev)
-        elif self.oldrev == '0000000000000000000000000000000000000000':
-            rep = '<Ref 0x%x creates %s on %s>' % (
-                  id(self), self.ref, self.newrev)
-        else:
-            # Catch all
-            rep = '<Ref 0x%x %s updated %s..%s>' % (
-                  id(self), self.ref, self.oldrev, self.newrev)
-
-        return rep
-
-    def equals(self, other):
-        if (self.project == other.project
-            and self.ref == other.ref
-            and self.newrev == other.newrev):
-            return True
-        return False
-
-    def isUpdateOf(self, other):
-        return False
-
-
-class NullChange(Changeish):
+class NullChange(Ref):
     # TODOv3(jeblair): remove this in favor of enqueueing Refs (eg
     # current master) instead.
     def __repr__(self):