Merge "Add multi-branch support for project-templates" into feature/zuulv3
diff --git a/doc/source/admin/drivers/gerrit.rst b/doc/source/admin/drivers/gerrit.rst
index ac42bd3..935cb32 100644
--- a/doc/source/admin/drivers/gerrit.rst
+++ b/doc/source/admin/drivers/gerrit.rst
@@ -61,6 +61,17 @@
 
       Path to Gerrit web interface.
 
+   .. attr:: gitweb_url_template
+      :default: {baseurl}/gitweb?p={project.name}.git;a=commitdiff;h={sha}
+
+      Url template for links to specific git shas. By default this will
+      point at Gerrit's built in gitweb but you can customize this value
+      to point elsewhere (like cgit or github).
+
+      The three values available for string interpolation are baseurl
+      which points back to Gerrit, project and all of its safe attributes,
+      and sha which is the git sha1.
+
    .. attr:: user
       :default: zuul
 
diff --git a/tests/fixtures/zuul-connections-cgit.conf b/tests/fixtures/zuul-connections-cgit.conf
new file mode 100644
index 0000000..39dc0bb
--- /dev/null
+++ b/tests/fixtures/zuul-connections-cgit.conf
@@ -0,0 +1,27 @@
+[gearman]
+server=127.0.0.1
+
+[scheduler]
+tenant_config=main.yaml
+
+[merger]
+git_dir=/tmp/zuul-test/merger-git
+git_user_email=zuul@example.com
+git_user_name=zuul
+
+[executor]
+git_dir=/tmp/zuul-test/executor-git
+
+[connection gerrit]
+driver=gerrit
+server=review.example.com
+user=jenkins
+sshkey=none
+gitweb_url_template=https://cgit.example.com/cgit/{project.name}/commit/?id={sha}
+
+[connection outgoing_smtp]
+driver=smtp
+server=localhost
+port=25
+default_from=zuul@example.com
+default_to=you@example.com
diff --git a/tests/fixtures/zuul-connections-gitweb.conf b/tests/fixtures/zuul-connections-gitweb.conf
new file mode 100644
index 0000000..172208e
--- /dev/null
+++ b/tests/fixtures/zuul-connections-gitweb.conf
@@ -0,0 +1,26 @@
+[gearman]
+server=127.0.0.1
+
+[scheduler]
+tenant_config=main.yaml
+
+[merger]
+git_dir=/tmp/zuul-test/merger-git
+git_user_email=zuul@example.com
+git_user_name=zuul
+
+[executor]
+git_dir=/tmp/zuul-test/executor-git
+
+[connection gerrit]
+driver=gerrit
+server=review.example.com
+user=jenkins
+sshkey=none
+
+[connection outgoing_smtp]
+driver=smtp
+server=localhost
+port=25
+default_from=zuul@example.com
+default_to=you@example.com
diff --git a/tests/unit/test_connection.py b/tests/unit/test_connection.py
index 719f307..c882d3a 100644
--- a/tests/unit/test_connection.py
+++ b/tests/unit/test_connection.py
@@ -338,3 +338,32 @@
         self.assertNotIn("sql", self.connections.connections)
         self.assertNotIn("timer", self.connections.connections)
         self.assertNotIn("zuul", self.connections.connections)
+
+
+class TestConnectionsCgit(ZuulTestCase):
+    config_file = 'zuul-connections-cgit.conf'
+    tenant_config_file = 'config/single-tenant/main.yaml'
+
+    def test_cgit_web_url(self):
+        self.assertIn("gerrit", self.connections.connections)
+        conn = self.connections.connections['gerrit']
+        source = conn.source
+        proj = source.getProject('foo/bar')
+        url = conn._getWebUrl(proj, '1')
+        self.assertEqual(url,
+                         'https://cgit.example.com/cgit/foo/bar/commit/?id=1')
+
+
+class TestConnectionsGitweb(ZuulTestCase):
+    config_file = 'zuul-connections-gitweb.conf'
+    tenant_config_file = 'config/single-tenant/main.yaml'
+
+    def test_gitweb_url(self):
+        self.assertIn("gerrit", self.connections.connections)
+        conn = self.connections.connections['gerrit']
+        source = conn.source
+        proj = source.getProject('foo/bar')
+        url = conn._getWebUrl(proj, '1')
+        url_should_be = 'https://review.example.com/' \
+                        'gitweb?p=foo/bar.git;a=commitdiff;h=1'
+        self.assertEqual(url, url_should_be)
diff --git a/zuul/driver/gerrit/gerritconnection.py b/zuul/driver/gerrit/gerritconnection.py
index c3f9ee2..59051bb 100644
--- a/zuul/driver/gerrit/gerritconnection.py
+++ b/zuul/driver/gerrit/gerritconnection.py
@@ -299,6 +299,12 @@
 
         self.baseurl = self.connection_config.get('baseurl',
                                                   'https://%s' % self.server)
+        default_gitweb_url_template = '{baseurl}/gitweb?' \
+                                      'p={project.name}.git;' \
+                                      'a=commitdiff;h={sha}'
+        url_template = self.connection_config.get('gitweb_url_template',
+                                                  default_gitweb_url_template)
+        self.gitweb_url_template = url_template
 
         self._change_cache = {}
         self.projects = {}
@@ -338,7 +344,7 @@
             change.ref = event.ref
             change.oldrev = event.oldrev
             change.newrev = event.newrev
-            change.url = self._getGitwebUrl(project, sha=event.newrev)
+            change.url = self._getWebUrl(project, sha=event.newrev)
         elif event.ref and not event.ref.startswith('refs/'):
             # Pre 2.13 Gerrit ref-updated events don't have branch prefixes.
             project = self.source.getProject(event.project_name)
@@ -347,7 +353,7 @@
             change.ref = 'refs/heads/' + event.ref
             change.oldrev = event.oldrev
             change.newrev = event.newrev
-            change.url = self._getGitwebUrl(project, sha=event.newrev)
+            change.url = self._getWebUrl(project, sha=event.newrev)
         elif event.ref and event.ref.startswith('refs/heads/'):
             # From the timer trigger or Post 2.13 Gerrit
             project = self.source.getProject(event.project_name)
@@ -356,7 +362,7 @@
             change.branch = event.ref[len('refs/heads/'):]
             change.oldrev = event.oldrev
             change.newrev = event.newrev
-            change.url = self._getGitwebUrl(project, sha=event.newrev)
+            change.url = self._getWebUrl(project, sha=event.newrev)
         elif event.ref:
             # catch-all ref (ie, not a branch or head)
             project = self.source.getProject(event.project_name)
@@ -364,7 +370,7 @@
             change.ref = event.ref
             change.oldrev = event.oldrev
             change.newrev = event.newrev
-            change.url = self._getGitwebUrl(project, sha=event.newrev)
+            change.url = self._getWebUrl(project, sha=event.newrev)
         else:
             self.log.warning("Unable to get change for %s" % (event,))
             change = None
@@ -848,11 +854,11 @@
                                      project.name)
         return url
 
-    def _getGitwebUrl(self, project: Project, sha: str=None) -> str:
-        url = '%s/gitweb?p=%s.git' % (self.baseurl, project.name)
-        if sha:
-            url += ';a=commitdiff;h=' + sha
-        return url
+    def _getWebUrl(self, project: Project, sha: str=None) -> str:
+        return self.gitweb_url_template.format(
+            baseurl=self.baseurl,
+            project=project.getSafeAttributes(),
+            sha=sha)
 
     def onLoad(self):
         self.log.debug("Starting Gerrit Connection/Watchers")
diff --git a/zuul/driver/github/githubconnection.py b/zuul/driver/github/githubconnection.py
index 55d3031..1186aca 100644
--- a/zuul/driver/github/githubconnection.py
+++ b/zuul/driver/github/githubconnection.py
@@ -135,6 +135,7 @@
     """Move events from GitHub into the scheduler"""
 
     log = logging.getLogger("zuul.GithubEventConnector")
+    delay = 3.0
 
     def __init__(self, connection):
         super(GithubEventConnector, self).__init__()
@@ -147,9 +148,17 @@
         self.connection.addEvent(None)
 
     def _handleEvent(self):
-        json_body, event_type = self.connection.getEvent()
+        ts, json_body, event_type = self.connection.getEvent()
         if self._stopped:
             return
+        # Github can produce inconsistent data immediately after an
+        # event, So ensure that we do not deliver the event to Zuul
+        # until at least a certain amount of time has passed.  Note
+        # that if we receive several events in succession, we will
+        # only need to delay for the first event.  In essence, Zuul
+        # should always be a constant number of seconds behind Github.
+        now = time.time()
+        time.sleep(max((ts + self.delay) - now, 0.0))
 
         # If there's any installation mapping information in the body then
         # update the project mapping before any requests are made.
@@ -543,7 +552,7 @@
         return token
 
     def addEvent(self, data, event=None):
-        return self.event_queue.put((data, event))
+        return self.event_queue.put((time.time(), data, event))
 
     def getEvent(self):
         return self.event_queue.get()
diff --git a/zuul/driver/sql/sqlreporter.py b/zuul/driver/sql/sqlreporter.py
index 7785c48..f9537ac 100644
--- a/zuul/driver/sql/sqlreporter.py
+++ b/zuul/driver/sql/sqlreporter.py
@@ -33,8 +33,8 @@
             return
 
         with self.connection.engine.begin() as conn:
-            change = getattr(item.change, 'number', '')
-            patchset = getattr(item.change, 'patchset', '')
+            change = getattr(item.change, 'number', None)
+            patchset = getattr(item.change, 'patchset', None)
             ref = getattr(item.change, 'ref', '')
             oldrev = getattr(item.change, 'oldrev', '')
             newrev = getattr(item.change, 'newrev', '')
diff --git a/zuul/model.py b/zuul/model.py
index 68df7fc..cf63f64 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -358,6 +358,9 @@
     def __repr__(self):
         return '<Project %s>' % (self.name)
 
+    def getSafeAttributes(self):
+        return Attributes(name=self.name)
+
 
 class Node(object):
     """A single node for use by a job.