Add prepare-gerrit-repos role and gerrit-base job

This role sets up the gerrit submodules, and the base job defines
all of the repos which are needed to build gerrit.  A test job
that inherits from it is added to exercise it.

Change-Id: Ibe5bb0866052c394a53f5f534daa6927a6880da2
diff --git a/playbooks/gerrit-base/pre.yaml b/playbooks/gerrit-base/pre.yaml
new file mode 100644
index 0000000..a15f0eb
--- /dev/null
+++ b/playbooks/gerrit-base/pre.yaml
@@ -0,0 +1,5 @@
+- name: Preparatory steps for all Gerrit build jobs
+  hosts: all
+  roles:
+    - ensure-bazelisk
+    - prepare-gerrit-repos
diff --git a/playbooks/test-gerrit-base.yaml b/playbooks/test-gerrit-base.yaml
new file mode 100644
index 0000000..eaa4fcf
--- /dev/null
+++ b/playbooks/test-gerrit-base.yaml
@@ -0,0 +1,20 @@
+- hosts: all
+  tasks:
+    - name: List submodule git dir
+      command: "ls src/gerrit.googlesource.com/gerrit/{{ item }}/.git"
+      args:
+        chdir: "{{ ansible_user_dir }}"
+      loop:
+        - modules/jgit
+        - plugins/codemirror-editor
+        - plugins/commit-message-length-validator
+        - plugins/delete-project
+        - plugins/download-commands
+        - plugins/gitiles
+        - plugins/hooks
+        - plugins/plugin-manager
+        - plugins/replication
+        - plugins/reviewnotes
+        - plugins/singleusergroup
+        - plugins/webhooks
+        - polymer-bridges
diff --git a/roles/prepare-gerrit-repos/README.rst b/roles/prepare-gerrit-repos/README.rst
new file mode 100644
index 0000000..81ec7bb
--- /dev/null
+++ b/roles/prepare-gerrit-repos/README.rst
@@ -0,0 +1,67 @@
+Prepare gerrit submodules
+
+Gerrit has a number of repos which are submodules of the Gerrit repo,
+and some plugins are expected to be built by being copied into the
+Gerrit repo.  This is generally compatible with the way Zuul operates,
+but special care needs to be taken.
+
+Zuul prepares the git repository states for all of the projects
+involved in testing a change.  These projects may include the project
+that the change is against, any other project if the change has a
+"Depends-On" footer pointing to a change in that other project, and
+any projects which the job specifies with "required-projects".  These
+git repository states represent the proposed future state of the
+world, in all branches, with dependent changes applied.
+
+This matches well with Gerrit's submodule subscription system, in that
+if a change to Gerrit "Depends-On" a change to a plugin, then as soon
+as that plugin change merges, The submodule in the Gerrit repo will be
+updated to the new plugin sha.  In other words, we can say with high
+confidence that the state of the world that was tested in Zuul is what
+actually resulted after the merge.
+
+However, Zuul itself does not perform any actions on submodules.  A
+simple "git submodule update --init" would discard the state of the
+repositories that Zuul prepared and would invalidate our testing.  But
+since Zuul has already prepared the repos, we don't need to use a "git
+submodule" command, we just need to move them into the correct
+location.  That is what this role does.
+
+There is one edge case: if a repo does not have the branch that is
+being tested, then Gerrit's submodule subscription does not work.
+That means that Zuul may not have checked out the same git sha as the
+submodule pointer in the gerrit repo, and we can not assume that if a
+change in a plugin lands, that the submodule pointer will be updated.
+In this case, this role falls back to performing a "git submodule
+update --init".  If, however, there is a dependent change in that
+repo, then this role will fail the job.  That is an untestable
+situation that can only be resolved by merging the dependent change
+and manually updating the submodule pointer in the Gerrit repo.
+
+The best way to avoid that situation is to ensure that all the
+dependent projects have the same branches as Gerrit itself.
+
+**Role Variables**
+
+.. zuul:rolevar:: gerrit_project_mapping
+   :type: dict
+
+   A dictionary to map Gerrit sub-projects to their location in the
+   gerrit repo.  This role iterates over every Zuul project and
+   assumes that it should be copied into the gerrit project with its
+   full project name.  For example, the `plugins/download-commands`
+   project will be copied into the ``plugins/download-commands``
+   directory under gerrit.  To specify an alternate location, add an
+   entry to this dictionary in the form ``project_name:
+   destination_dir``.  To omit copying the project into the gerrit
+   repo, supply the empty string.
+
+   The following is the default value; it instructs the role not to
+   copy the gerrit project into itself, and to copy the jgit project
+   into ``modules/jgit``:
+
+   .. code-block:: yaml
+
+      gerrit_project_mapping:
+        gerrit: ''
+        jgit: modules/jgit
diff --git a/roles/prepare-gerrit-repos/defaults/main.yaml b/roles/prepare-gerrit-repos/defaults/main.yaml
new file mode 100644
index 0000000..ce0cbe7
--- /dev/null
+++ b/roles/prepare-gerrit-repos/defaults/main.yaml
@@ -0,0 +1,4 @@
+gerrit_project_mapping:
+  gerrit: ''
+  jgit: modules/jgit
+  'zuul/jobs': ''
diff --git a/roles/prepare-gerrit-repos/tasks/main.yaml b/roles/prepare-gerrit-repos/tasks/main.yaml
new file mode 100644
index 0000000..d32320e
--- /dev/null
+++ b/roles/prepare-gerrit-repos/tasks/main.yaml
@@ -0,0 +1,20 @@
+# In case there is no matching branch we may need to check out the
+# actual sha defined in the parent repo. The default zuul remote,
+# file:///dev/null, doesn't work here because relative paths cause
+# it to be file:///dev/plugins/download-commands, which isn't a
+# thing. Removing the origin causes git to use relative local
+# filesystem paths.
+- name: Remove origin remote
+  command: "git remote rm origin"
+  args:
+    chdir: "{{ gerrit_root }}"
+
+- name: Move plugin repos into gerrit tree
+  include_tasks:
+    file: repo.yaml
+  vars:
+    project_dest: "{{ gerrit_project_mapping.get(project.name, project.name) }}"
+  when: "project_dest != ''"
+  loop: "{{ zuul.projects.values() | list }}"
+  loop_control:
+    loop_var: project
diff --git a/roles/prepare-gerrit-repos/tasks/repo.yaml b/roles/prepare-gerrit-repos/tasks/repo.yaml
new file mode 100644
index 0000000..f4da53f
--- /dev/null
+++ b/roles/prepare-gerrit-repos/tasks/repo.yaml
@@ -0,0 +1,44 @@
+- name: Prepare project
+  debug:
+    msg: "{{ project.name }} {{ project_dest }}"
+
+- name: Check if zuul.branch exists in repo
+  set_fact:
+    # If zuul checked out the branch we're testing, then it exists.
+    project_branch_exists: "{{ zuul.branch == project.checkout }}"
+
+- name: Check if repo has a dependent change
+  set_fact:
+    repo_has_dependent_change: "{{ zuul['items'] | selectattr('project.canonical_name', 'eq', zuul.project.canonical_name) | list | length | bool }}"
+
+- name: Check for unsatisfiable source repo condition
+  when:
+    - "project.canonical_name != zuul.project.canonical_name"
+    - "not project_branch_exists"
+    - "repo_has_dependent_change"
+  fail:
+    msg: >-
+      The repository {{ project.name }} does not contain the branch
+      under test ({{ zuul.branch }}), but this change depends on a
+      change to that project and branch.  While Zuul is able to check
+      out the repos in the requested state, the branch mismatch means
+      that Gerrit's submodule subscription would not automatically
+      update the submodule pointer, and the merged state would not
+      reflect the tested state.
+
+      This configuration would be testable by creating a {{
+      zuul.branch }} branch in the {{ project.name }} repo.
+      Alternatively, you can merge the dependent change, manually
+      update the submodule pointer, then test this change again.
+
+# If there is no matching branch we need to check out the actual sha
+# defined in the parent repo.
+- name: Update submodule
+  when: "not project_branch_exists"
+  command: "git submodule update --init {{ project.name }}"
+  args:
+    chdir: "{{ gerrit_root }}"
+
+- name: Move repo into place
+  when: "project_branch_exists"
+  command: "mv -T -f {{ ansible_user_dir }}/{{ project.src_dir }} {{ gerrit_root }}/{{ project_dest }}"
diff --git a/roles/prepare-gerrit-repos/vars/main.yaml b/roles/prepare-gerrit-repos/vars/main.yaml
new file mode 100644
index 0000000..9f72e90
--- /dev/null
+++ b/roles/prepare-gerrit-repos/vars/main.yaml
@@ -0,0 +1 @@
+gerrit_root: "{{ ansible_user_dir }}/{{ zuul.projects['gerrit.googlesource.com/gerrit'].src_dir }}"
diff --git a/zuul.d/jobs.yaml b/zuul.d/jobs.yaml
new file mode 100644
index 0000000..8107e4f
--- /dev/null
+++ b/zuul.d/jobs.yaml
@@ -0,0 +1,22 @@
+- job:
+    name: gerrit-base
+    description: |
+      Base job for building gerrit
+
+      This job sets up all the repos which are required for a gerrit build.
+    pre-run: playbooks/gerrit-base/pre.yaml
+    required-projects:
+      - gerrit
+      - jgit
+      - plugins/codemirror-editor
+      - plugins/commit-message-length-validator
+      - plugins/delete-project
+      - plugins/download-commands
+      - plugins/gitiles
+      - plugins/hooks
+      - plugins/plugin-manager
+      - plugins/replication
+      - plugins/reviewnotes
+      - plugins/singleusergroup
+      - plugins/webhooks
+      - polymer-bridges
diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml
index 73a1f7f..2c55f4a 100644
--- a/zuul.d/project.yaml
+++ b/zuul.d/project.yaml
@@ -2,3 +2,4 @@
     check:
       jobs:
         - test-ensure-bazelisk
+        - test-gerrit-base
diff --git a/zuul.d/test-jobs.yaml b/zuul.d/test-jobs.yaml
index 52002b4..e6765df 100644
--- a/zuul.d/test-jobs.yaml
+++ b/zuul.d/test-jobs.yaml
@@ -14,3 +14,13 @@
     files:
       - roles/ensure-bazelisk/.*
       - playbooks/test-ensure-bazelisk.yaml
+
+- job:
+    name: test-gerrit-base
+    parent: gerrit-base
+    run: playbooks/test-gerrit-base.yaml
+    nodeset: temp-debian
+    files:
+      - roles/ensure-bazelisk/.*
+      - roles/prepare-gerrit-repos/.*
+      - playbooks/test-gerrit-base.yaml