Merge "Adding doc reference of ZUUL_CHANGES"
diff --git a/etc/status/public_html/zuul.app.js b/etc/status/public_html/zuul.app.js
index 6f87a92..640437b 100644
--- a/etc/status/public_html/zuul.app.js
+++ b/etc/status/public_html/zuul.app.js
@@ -39,6 +39,9 @@
     });
 }
 
+/**
+ * @return The $.zuul instance
+ */
 function zuul_start($) {
     // Start the zuul app (expects default dom)
 
@@ -94,4 +97,6 @@
             }
         });
     });
-}
\ No newline at end of file
+
+    return zuul;
+}
diff --git a/requirements.txt b/requirements.txt
index f5525b6..c682999 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -11,7 +11,7 @@
 extras
 statsd>=1.0.0,<3.0
 voluptuous>=0.7
-gear>=0.5.4,<1.0.0
+gear>=0.5.7,<1.0.0
 apscheduler>=2.1.1,<3.0
 PrettyTable>=0.6,<0.8
 babel>=1.0
diff --git a/tests/base.py b/tests/base.py
index becc854..8c96d18 100755
--- a/tests/base.py
+++ b/tests/base.py
@@ -1116,6 +1116,12 @@
         while len(self.gearman_server.functions) < count:
             time.sleep(0)
 
+    def orderedRelease(self):
+        # Run one build at a time to ensure non-race order:
+        while len(self.builds):
+            self.release(self.builds[0])
+            self.waitUntilSettled()
+
     def release(self, job):
         if isinstance(job, FakeBuild):
             job.release()
diff --git a/tests/fixtures/layout-no-timer.yaml b/tests/fixtures/layout-no-timer.yaml
index 9436821..ca40d13 100644
--- a/tests/fixtures/layout-no-timer.yaml
+++ b/tests/fixtures/layout-no-timer.yaml
@@ -1,14 +1,28 @@
 pipelines:
+  - name: check
+    manager: IndependentPipelineManager
+    trigger:
+      gerrit:
+        - event: patchset-created
+    success:
+      gerrit:
+        verified: 1
+    failure:
+      gerrit:
+        verified: -1
+
   - name: periodic
     manager: IndependentPipelineManager
     # Trigger is required, set it to one that is a noop
     # during tests that check the timer trigger.
     trigger:
       gerrit:
-        - event: patchset-created
+        - event: ref-updated
 
 projects:
   - name: org/project
+    check:
+      - project-test1
     periodic:
       - project-bitrot-stable-old
       - project-bitrot-stable-older
diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py
index 0779bfa..3b59e3e 100755
--- a/tests/test_scheduler.py
+++ b/tests/test_scheduler.py
@@ -1710,6 +1710,41 @@
         self.assertEqual(A.reported, 0, "Abandoned change should not report")
         self.assertEqual(B.reported, 1, "Change should report")
 
+    def test_abandoned_not_timer(self):
+        "Test that an abandoned change does not cancel timer jobs"
+
+        self.worker.hold_jobs_in_build = True
+
+        # Start timer trigger - also org/project
+        self.config.set('zuul', 'layout_config',
+                        'tests/fixtures/layout-idle.yaml')
+        self.sched.reconfigure(self.config)
+        self.registerJobs()
+        # The pipeline triggers every second, so we should have seen
+        # several by now.
+        time.sleep(5)
+        self.waitUntilSettled()
+        # Stop queuing timer triggered jobs so that the assertions
+        # below don't race against more jobs being queued.
+        self.config.set('zuul', 'layout_config',
+                        'tests/fixtures/layout-no-timer.yaml')
+        self.sched.reconfigure(self.config)
+        self.registerJobs()
+        self.assertEqual(len(self.builds), 2, "Two timer jobs")
+
+        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+        self.waitUntilSettled()
+        self.assertEqual(len(self.builds), 3, "One change plus two timer jobs")
+
+        self.fake_gerrit.addEvent(A.getChangeAbandonedEvent())
+        self.waitUntilSettled()
+
+        self.assertEqual(len(self.builds), 2, "Two timer jobs remain")
+
+        self.worker.release()
+        self.waitUntilSettled()
+
     def test_zuul_url_return(self):
         "Test if ZUUL_URL is returning when zuul_url is set in zuul.conf"
         self.assertTrue(self.sched.config.has_option('merger', 'zuul_url'))
@@ -2089,9 +2124,7 @@
         self.waitUntilSettled()
 
         # Run one build at a time to ensure non-race order:
-        for x in range(6):
-            self.release(self.builds[0])
-            self.waitUntilSettled()
+        self.orderedRelease()
         self.worker.hold_jobs_in_build = False
         self.waitUntilSettled()
 
@@ -2120,7 +2153,10 @@
         self.assertIn('Content-Type', headers)
         self.assertEqual(headers['Content-Type'],
                          'application/json; charset=UTF-8')
+        self.assertIn('Access-Control-Allow-Origin', headers)
+        self.assertIn('Cache-Control', headers)
         self.assertIn('Last-Modified', headers)
+        self.assertIn('Expires', headers)
         data = f.read()
 
         self.worker.hold_jobs_in_build = False
@@ -3387,7 +3423,7 @@
 
     def test_crd_check_git_depends(self):
         "Test single-repo dependencies in independent pipelines"
-        self.gearman_server.hold_jobs_in_queue = True
+        self.gearman_server.hold_jobs_in_build = True
         A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
         B = self.fake_gerrit.addFakeChange('org/project1', 'master', 'B')
 
@@ -3399,8 +3435,8 @@
         self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
         self.waitUntilSettled()
 
-        self.gearman_server.hold_jobs_in_queue = False
-        self.gearman_server.release()
+        self.orderedRelease()
+        self.gearman_server.hold_jobs_in_build = False
         self.waitUntilSettled()
 
         self.assertEqual(A.data['status'], 'NEW')
@@ -3417,7 +3453,7 @@
 
     def test_crd_check_duplicate(self):
         "Test duplicate check in independent pipelines"
-        self.gearman_server.hold_jobs_in_queue = True
+        self.worker.hold_jobs_in_build = True
         A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
         B = self.fake_gerrit.addFakeChange('org/project1', 'master', 'B')
         check_pipeline = self.sched.layout.pipelines['check']
@@ -3440,13 +3476,9 @@
 
         # Release jobs in order to avoid races with change A jobs
         # finishing before change B jobs.
-        self.gearman_server.release('.*-merge')
-        self.gearman_server.release('project1-.*')
-        self.waitUntilSettled()
-        self.gearman_server.release('.*-merge')
-        self.gearman_server.release('project1-.*')
-        self.waitUntilSettled()
-        self.gearman_server.release()
+        self.orderedRelease()
+        self.worker.hold_jobs_in_build = False
+        self.worker.release()
         self.waitUntilSettled()
 
         self.assertEqual(A.data['status'], 'NEW')
diff --git a/zuul/model.py b/zuul/model.py
index 8dc28df..4d402ff 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -965,7 +965,8 @@
         return None
 
     def equals(self, other):
-        if (self.project == other.project):
+        if (self.project == other.project
+            and other._id() is None):
             return True
         return False
 
diff --git a/zuul/trigger/gerrit.py b/zuul/trigger/gerrit.py
index a99db4d..c28401c 100644
--- a/zuul/trigger/gerrit.py
+++ b/zuul/trigger/gerrit.py
@@ -408,18 +408,19 @@
         change.branch = data['branch']
         change.url = data['url']
         max_ps = 0
-        change.files = []
+        files = []
         for ps in data['patchSets']:
             if ps['number'] == change.patchset:
                 change.refspec = ps['ref']
                 for f in ps.get('files', []):
-                    change.files.append(f['file'])
+                    files.append(f['file'])
             if int(ps['number']) > int(max_ps):
                 max_ps = ps['number']
         if max_ps == change.patchset:
             change.is_current_patchset = True
         else:
             change.is_current_patchset = False
+        change.files = files
 
         change.is_merged = self._isMerged(change)
         change.approvals = data['currentPatchSet'].get('approvals', [])
@@ -438,7 +439,7 @@
             history = history[:]
         history.append(change.number)
 
-        change.needs_changes = []
+        needs_changes = []
         if 'dependsOn' in data:
             parts = data['dependsOn'][0]['ref'].split('/')
             dep_num, dep_ps = parts[3], parts[4]
@@ -448,8 +449,8 @@
             self.log.debug("Getting git-dependent change %s,%s" %
                            (dep_num, dep_ps))
             dep = self._getChange(dep_num, dep_ps, history=history)
-            if (not dep.is_merged) and dep not in change.needs_changes:
-                change.needs_changes.append(dep)
+            if (not dep.is_merged) and dep not in needs_changes:
+                needs_changes.append(dep)
 
         for record in self._getDependsOnFromCommit(data['commitMessage']):
             dep_num = record['number']
@@ -460,17 +461,18 @@
             self.log.debug("Getting commit-dependent change %s,%s" %
                            (dep_num, dep_ps))
             dep = self._getChange(dep_num, dep_ps, history=history)
-            if (not dep.is_merged) and dep not in change.needs_changes:
-                change.needs_changes.append(dep)
+            if (not dep.is_merged) and dep not in needs_changes:
+                needs_changes.append(dep)
+        change.needs_changes = needs_changes
 
-        change.needed_by_changes = []
+        needed_by_changes = []
         if 'neededBy' in data:
             for needed in data['neededBy']:
                 parts = needed['ref'].split('/')
                 dep_num, dep_ps = parts[3], parts[4]
                 dep = self._getChange(dep_num, dep_ps)
                 if (not dep.is_merged) and dep.is_current_patchset:
-                    change.needed_by_changes.append(dep)
+                    needed_by_changes.append(dep)
 
         for record in self._getNeededByFromCommit(data['id']):
             dep_num = record['number']
@@ -483,7 +485,8 @@
             # change).
             dep = self._getChange(dep_num, dep_ps, refresh=True)
             if (not dep.is_merged) and dep.is_current_patchset:
-                change.needed_by_changes.append(dep)
+                needed_by_changes.append(dep)
+        change.needed_by_changes = needed_by_changes
 
         return change
 
diff --git a/zuul/webapp.py b/zuul/webapp.py
index e289398..44c333b 100644
--- a/zuul/webapp.py
+++ b/zuul/webapp.py
@@ -121,5 +121,10 @@
                 raise webob.exc.HTTPNotFound()
 
         response.headers['Access-Control-Allow-Origin'] = '*'
+
+        response.cache_control.public = True
+        response.cache_control.max_age = self.cache_expiry
         response.last_modified = self.cache_time
-        return response
+        response.expires = self.cache_time + self.cache_expiry
+
+        return response.conditional_response_app