Merge "Use yarn and webpack to manage zuul-web javascript"
diff --git a/bindep.txt b/bindep.txt
index 3dcc3e7..11ebdf5 100644
--- a/bindep.txt
+++ b/bindep.txt
@@ -13,7 +13,7 @@
 openssl-devel [platform:rpm]
 libffi-dev [platform:dpkg]
 libffi-devel [platform:rpm]
-python-dev [platform:dpkg]
-python-devel [platform:rpm]
+python3-dev [platform:dpkg]
+python3-devel [platform:rpm]
 bubblewrap [platform:rpm]
 redhat-rpm-config [platform:rpm]
diff --git a/doc/source/user/config.rst b/doc/source/user/config.rst
index 0932c56..36cd68c 100644
--- a/doc/source/user/config.rst
+++ b/doc/source/user/config.rst
@@ -877,14 +877,14 @@
       same name will override a previously defined variable, but new
       variable names will be added to the set of defined variables.
 
-   .. attr:: host_vars
+   .. attr:: host-vars
 
       A dictionary of host variables to supply to Ansible.  The keys
       of this dictionary are node names as defined in a
       :ref:`nodeset`, and the values are dictionaries of variables,
       just as in :attr:`job.vars`.
 
-   .. attr:: group_vars
+   .. attr:: group-vars
 
       A dictionary of group variables to supply to Ansible.  The keys
       of this dictionary are node groups as defined in a
@@ -912,10 +912,10 @@
                   - api2
          vars:
            foo: "this variable is visible to all nodes"
-         host_vars:
+         host-vars:
            controller:
              bar: "this variable is visible only on the controller node"
-         group_vars:
+         group-vars:
            api:
              baz: "this variable is visible on api1 and api2"
 
diff --git a/tests/fixtures/config/ansible/git/common-config/zuul.yaml b/tests/fixtures/config/ansible/git/common-config/zuul.yaml
index 13a19da..abd77ec 100644
--- a/tests/fixtures/config/ansible/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/ansible/git/common-config/zuul.yaml
@@ -138,10 +138,10 @@
             - host3
     vars:
       allvar: all
-    host_vars:
+    host-vars:
       host1:
         hostvar: host
-    group_vars:
+    group-vars:
       group1:
         groupvar: group
 
diff --git a/tests/unit/test_connection.py b/tests/unit/test_connection.py
index c45da94..bacdf8f 100644
--- a/tests/unit/test_connection.py
+++ b/tests/unit/test_connection.py
@@ -74,7 +74,7 @@
         buildset_table = table_prefix + 'zuul_buildset'
         build_table = table_prefix + 'zuul_build'
 
-        self.assertEqual(13, len(insp.get_columns(buildset_table)))
+        self.assertEqual(14, len(insp.get_columns(buildset_table)))
         self.assertEqual(10, len(insp.get_columns(build_table)))
 
     def test_sql_results(self):
@@ -139,6 +139,7 @@
                 uuid=buildset0_builds[0]['uuid']),
             buildset0_builds[0]['log_url'])
         self.assertEqual('check', buildset1['pipeline'])
+        self.assertEqual('master', buildset1['branch'])
         self.assertEqual('org/project', buildset1['project'])
         self.assertEqual(2, buildset1['change'])
         self.assertEqual('1', buildset1['patchset'])
diff --git a/tests/unit/test_scheduler.py b/tests/unit/test_scheduler.py
index c833fa2..b640e33 100755
--- a/tests/unit/test_scheduler.py
+++ b/tests/unit/test_scheduler.py
@@ -3577,6 +3577,56 @@
         self.assertEqual(len(self.history), 0)
         self.assertEqual(len(self.builds), 0)
 
+    def test_client_enqueue_ref_negative(self):
+        "Test that the RPC client returns errors"
+        client = zuul.rpcclient.RPCClient('127.0.0.1',
+                                          self.gearman_server.port)
+        self.addCleanup(client.shutdown)
+        with testtools.ExpectedException(zuul.rpcclient.RPCFailure,
+                                         "New rev must be 40 character sha1"):
+            r = client.enqueue_ref(
+                tenant='tenant-one',
+                pipeline='post',
+                project='org/project',
+                trigger='gerrit',
+                ref='master',
+                oldrev='90f173846e3af9154517b88543ffbd1691f31366',
+                newrev='10054041')
+            self.assertEqual(r, False)
+        with testtools.ExpectedException(zuul.rpcclient.RPCFailure,
+                                         "Old rev must be 40 character sha1"):
+            r = client.enqueue_ref(
+                tenant='tenant-one',
+                pipeline='post',
+                project='org/project',
+                trigger='gerrit',
+                ref='master',
+                oldrev='10054041',
+                newrev='90f173846e3af9154517b88543ffbd1691f31366')
+            self.assertEqual(r, False)
+        with testtools.ExpectedException(zuul.rpcclient.RPCFailure,
+                                         "New rev must be base16 hash"):
+            r = client.enqueue_ref(
+                tenant='tenant-one',
+                pipeline='post',
+                project='org/project',
+                trigger='gerrit',
+                ref='master',
+                oldrev='90f173846e3af9154517b88543ffbd1691f31366',
+                newrev='notbase16')
+            self.assertEqual(r, False)
+        with testtools.ExpectedException(zuul.rpcclient.RPCFailure,
+                                         "Old rev must be base16 hash"):
+            r = client.enqueue_ref(
+                tenant='tenant-one',
+                pipeline='post',
+                project='org/project',
+                trigger='gerrit',
+                ref='master',
+                oldrev='notbase16',
+                newrev='90f173846e3af9154517b88543ffbd1691f31366')
+            self.assertEqual(r, False)
+
     def test_client_promote(self):
         "Test that the RPC client can promote a change"
         self.executor_server.hold_jobs_in_build = True
diff --git a/tools/test-logs.sh b/tools/test-logs.sh
old mode 100644
new mode 100755
index a514dd8..429bac5
--- a/tools/test-logs.sh
+++ b/tools/test-logs.sh
@@ -15,10 +15,25 @@
 # limitations under the License.
 
 ZUUL_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )"
-ARA_DIR=$(dirname $(python3 -c 'import ara ; print(ara.__file__)'))
-WORK_DIR=$PWD/test-logs-output
+# Initialize tox environment if it's not set up
+if [[ ! -d "${ZUUL_DIR}/.tox/venv" ]]; then
+    pushd $ZUUL_DIR
+        echo "Virtualenv doesn't exist... creating."
+        tox -e venv --notest
+    popd
+fi
+# Source tox environment
+source ${ZUUL_DIR}/.tox/venv/bin/activate
 
-mkdir -p $WORK_DIR
+# Install ARA if it's not installed (not in requirements.txt by default)
+python -c "import ara" &> /dev/null
+if [ $? -eq 1 ]; then
+    echo "ARA isn't installed... Installing it."
+    pip install ara
+fi
+ARA_DIR=$(dirname $(python3 -c 'import ara; print(ara.__file__)'))
+
+WORK_DIR=$(mktemp -d /tmp/zuul_logs_XXXX)
 
 if [ -z $1 ] ; then
     INVENTORY=$WORK_DIR/hosts.yaml
@@ -26,11 +41,11 @@
 all:
   hosts:
     controller:
-      ansible_host: localhost
+      ansible_connection: local
     node1:
-      ansible_host: localhost
+      ansible_connection: local
     node2:
-      ansible_host: localhost
+      ansible_connection: local
 node:
   hosts:
     node1: null
@@ -63,4 +78,5 @@
 rm -rf $ARA_DIR
 ansible-playbook $ZUUL_DIR/playbooks/zuul-stream/fixtures/test-stream.yaml
 ansible-playbook $ZUUL_DIR/playbooks/zuul-stream/fixtures/test-stream-failure.yaml
+# ansible-playbook $ZUUL_DIR/playbooks/zuul-stream/functional.yaml
 echo "Logs are in $WORK_DIR"
diff --git a/zuul/configloader.py b/zuul/configloader.py
index 3511f96..d3f3236 100644
--- a/zuul/configloader.py
+++ b/zuul/configloader.py
@@ -512,8 +512,8 @@
                       'roles': to_list(role),
                       'required-projects': to_list(vs.Any(job_project, str)),
                       'vars': dict,
-                      'host_vars': {str: dict},
-                      'group_vars': {str: dict},
+                      'host-vars': {str: dict},
+                      'group-vars': {str: dict},
                       'dependencies': to_list(str),
                       'allowed-projects': to_list(str),
                       'override-branch': str,
@@ -748,14 +748,14 @@
                 raise Exception("Variables named 'zuul' or 'nodepool' "
                                 "are not allowed.")
             job.variables = variables
-        host_variables = conf.get('host_vars', None)
+        host_variables = conf.get('host-vars', None)
         if host_variables:
             for host, hvars in host_variables.items():
                 if 'zuul' in hvars or 'nodepool' in hvars:
                     raise Exception("Variables named 'zuul' or 'nodepool' "
                                     "are not allowed.")
             job.host_variables = host_variables
-        group_variables = conf.get('group_vars', None)
+        group_variables = conf.get('group-vars', None)
         if group_variables:
             for group, gvars in group_variables.items():
                 if 'zuul' in group_variables or 'nodepool' in gvars:
diff --git a/zuul/driver/sql/alembic/versions/defa75d297bf_add_branch_column.py b/zuul/driver/sql/alembic/versions/defa75d297bf_add_branch_column.py
new file mode 100644
index 0000000..714975e
--- /dev/null
+++ b/zuul/driver/sql/alembic/versions/defa75d297bf_add_branch_column.py
@@ -0,0 +1,39 @@
+# Copyright 2018 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""Add branch column
+
+Revision ID: defa75d297bf
+Revises: 19d3a3ebfe1d
+Create Date: 2018-02-21 01:52:23.781875
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = 'defa75d297bf'
+down_revision = '19d3a3ebfe1d'
+branch_labels = None
+depends_on = None
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade(table_prefix=''):
+    op.add_column(
+        table_prefix + 'zuul_buildset', sa.Column('branch', sa.String(255)))
+
+
+def downgrade():
+    raise Exception("Downgrades not supported")
diff --git a/zuul/driver/sql/sqlconnection.py b/zuul/driver/sql/sqlconnection.py
index e931301..ab387a6 100644
--- a/zuul/driver/sql/sqlconnection.py
+++ b/zuul/driver/sql/sqlconnection.py
@@ -96,6 +96,7 @@
             sa.Column('zuul_ref', sa.String(255)),
             sa.Column('pipeline', sa.String(255)),
             sa.Column('project', sa.String(255)),
+            sa.Column('branch', sa.String(255)),
             sa.Column('change', sa.Integer, nullable=True),
             sa.Column('patchset', sa.String(255), nullable=True),
             sa.Column('ref', sa.String(255)),
@@ -156,7 +157,7 @@
 
 class SqlWebHandler(BaseWebHandler):
     log = logging.getLogger("zuul.web.SqlHandler")
-    filters = ("project", "pipeline", "change", "patchset", "ref",
+    filters = ("project", "pipeline", "change", "branch", "patchset", "ref",
                "result", "uuid", "job_name", "voting", "node_name", "newrev")
 
     def __init__(self, connection, zuul_web, method, path):
@@ -168,6 +169,7 @@
         buildset = self.connection.zuul_buildset_table
         query = select([
             buildset.c.project,
+            buildset.c.branch,
             buildset.c.pipeline,
             buildset.c.change,
             buildset.c.patchset,
@@ -222,7 +224,7 @@
                 'skip': 0,
             }
             for k, v in urllib.parse.parse_qsl(request.rel_url.query_string):
-                if k in ("tenant", "project", "pipeline", "change",
+                if k in ("tenant", "project", "pipeline", "change", "branch",
                          "patchset", "ref", "newrev"):
                     args['buildset_filters'].setdefault(k, []).append(v)
                 elif k in ("uuid", "job_name", "voting", "node_name",
diff --git a/zuul/driver/sql/sqlreporter.py b/zuul/driver/sql/sqlreporter.py
index f9537ac..9932ff0 100644
--- a/zuul/driver/sql/sqlreporter.py
+++ b/zuul/driver/sql/sqlreporter.py
@@ -52,6 +52,7 @@
                 message=self._formatItemReport(
                     item, with_jobs=False),
                 tenant=item.pipeline.layout.tenant.name,
+                branch=item.change.branch,
             )
             buildset_ins_result = conn.execute(buildset_ins)
             build_inserts = []
diff --git a/zuul/rpclistener.py b/zuul/rpclistener.py
index f3f55f6..7c777fd 100644
--- a/zuul/rpclistener.py
+++ b/zuul/rpclistener.py
@@ -235,6 +235,22 @@
             event.ref = args['ref']
             event.oldrev = args['oldrev']
             event.newrev = args['newrev']
+            try:
+                int(event.oldrev, 16)
+                if len(event.oldrev) != 40:
+                    errors += 'Old rev must be 40 character sha1: ' \
+                              '%s\n' % event.oldrev
+            except Exception:
+                errors += 'Old rev must be base16 hash: ' \
+                          '%s\n' % event.oldrev
+            try:
+                int(event.newrev, 16)
+                if len(event.newrev) != 40:
+                    errors += 'New rev must be 40 character sha1: ' \
+                              '%s\n' % event.newrev
+            except Exception:
+                errors += 'New rev must be base16 hash: ' \
+                          '%s\n' % event.newrev
 
         if errors:
             job.sendWorkException(errors.encode('utf8'))