Merge "Add support for override-checkout, deprecate override-branch" into feature/zuulv3
diff --git a/doc/source/user/config.rst b/doc/source/user/config.rst
index 0019932..fa874a9 100644
--- a/doc/source/user/config.rst
+++ b/doc/source/user/config.rst
@@ -744,7 +744,7 @@
       If a job has an empty or no nodeset definition, it will still
       run and may be able to perform actions on the Zuul executor.
 
-   .. attr:: override-branch
+   .. attr:: override-checkout
 
       When Zuul runs jobs for a proposed change, it normally checks
       out the branch associated with that change on every project
@@ -752,13 +752,13 @@
       branch tip or tag), then that ref is normally checked out.  This
       attribute is used to override that behavior and indicate that
       this job should, regardless of the branch for the queue item,
-      use the indicated branch instead.  This can be used, for
-      example, to run a previous version of the software (from a
-      stable maintenance branch) under test even if the change being
-      tested applies to a different branch (this is only likely to be
-      useful if there is some cross-branch interaction with some
+      use the indicated ref (i.e., branch or tag) instead.  This can
+      be used, for example, to run a previous version of the software
+      (from a stable maintenance branch) under test even if the change
+      being tested applies to a different branch (this is only likely
+      to be useful if there is some cross-branch interaction with some
       component of the system being tested).  See also the
-      project-specific :attr:`job.required-projects.override-branch`
+      project-specific :attr:`job.required-projects.override-checkout`
       attribute to apply this behavior to a subset of a job's
       projects.
 
@@ -915,7 +915,7 @@
 
          The name of the required project.
 
-      .. attr:: override-branch
+      .. attr:: override-checkout
 
          When Zuul runs jobs for a proposed change, it normally checks
          out the branch associated with that change on every project
@@ -923,9 +923,10 @@
          branch tip or tag), then that ref is normally checked out.
          This attribute is used to override that behavior and indicate
          that this job should, regardless of the branch for the queue
-         item, use the indicated branch instead, for only this
-         project.  See also the :attr:`job.override-branch` attribute
-         to apply the same behavior to all projects in a job.
+         item, use the indicated ref (i.e., branch or tag) instead,
+         for only this project.  See also the
+         :attr:`job.override-checkout` attribute to apply the same
+         behavior to all projects in a job.
 
    .. attr:: vars
 
diff --git a/tests/fixtures/layouts/repo-checkout-no-timer-override.yaml b/tests/fixtures/layouts/repo-checkout-no-timer-override.yaml
index ca49292..89d2b93 100644
--- a/tests/fixtures/layouts/repo-checkout-no-timer-override.yaml
+++ b/tests/fixtures/layouts/repo-checkout-no-timer-override.yaml
@@ -15,7 +15,7 @@
 - job:
     name: integration
     branches: master
-    override-branch: stable/havana
+    override-checkout: stable/havana
     required-projects:
       - org/project1
     run: playbooks/integration.yaml
diff --git a/tests/fixtures/layouts/repo-checkout-no-timer.yaml b/tests/fixtures/layouts/repo-checkout-no-timer.yaml
index 6b88801..0374897 100644
--- a/tests/fixtures/layouts/repo-checkout-no-timer.yaml
+++ b/tests/fixtures/layouts/repo-checkout-no-timer.yaml
@@ -14,7 +14,7 @@
 
 - job:
     name: integration
-    override-branch: stable/havana
+    override-checkout: stable/havana
     required-projects:
       - org/project1
     run: playbooks/integration.yaml
diff --git a/tests/fixtures/layouts/repo-checkout-six-project.yaml b/tests/fixtures/layouts/repo-checkout-six-project.yaml
index 6079612..4878665 100644
--- a/tests/fixtures/layouts/repo-checkout-six-project.yaml
+++ b/tests/fixtures/layouts/repo-checkout-six-project.yaml
@@ -44,7 +44,7 @@
       - org/project2
       - org/project3
       - name: org/project4
-        override-branch: master
+        override-checkout: master
       - org/project5
       - org/project6
     run: playbooks/integration.yaml
diff --git a/tests/fixtures/layouts/repo-checkout-tag.yaml b/tests/fixtures/layouts/repo-checkout-tag.yaml
new file mode 100644
index 0000000..3f1af1c
--- /dev/null
+++ b/tests/fixtures/layouts/repo-checkout-tag.yaml
@@ -0,0 +1,36 @@
+- pipeline:
+    name: check
+    manager: independent
+    trigger:
+      gerrit:
+        - event: patchset-created
+    success:
+      gerrit:
+        Verified: 1
+    failure:
+      gerrit:
+        Verified: -1
+
+- job:
+    name: base
+    parent: null
+
+- job:
+    name: integration
+    required-projects:
+      - org/project1
+      - name: org/project2
+        override-checkout: test-tag
+    run: playbooks/integration.yaml
+
+- project:
+    name: org/project1
+    check:
+      jobs:
+        - integration
+
+- project:
+    name: org/project2
+    check:
+      jobs:
+        - integration
diff --git a/tests/fixtures/layouts/repo-checkout-timer-override.yaml b/tests/fixtures/layouts/repo-checkout-timer-override.yaml
index af5bd3c..4aacfee 100644
--- a/tests/fixtures/layouts/repo-checkout-timer-override.yaml
+++ b/tests/fixtures/layouts/repo-checkout-timer-override.yaml
@@ -13,7 +13,7 @@
 - job:
     name: integration
     branches: master
-    override-branch: stable/havana
+    override-checkout: stable/havana
     required-projects:
       - org/project1
     run: playbooks/integration.yaml
diff --git a/tests/unit/test_executor.py b/tests/unit/test_executor.py
index f051ec4..5d27663 100755
--- a/tests/unit/test_executor.py
+++ b/tests/unit/test_executor.py
@@ -378,6 +378,32 @@
 
         self.assertBuildStates(states, projects)
 
+    @simple_layout('layouts/repo-checkout-tag.yaml')
+    def test_tag_checkout(self):
+        self.executor_server.hold_jobs_in_build = True
+        p1 = "review.example.com/org/project1"
+        p2 = "review.example.com/org/project2"
+        projects = [p1, p2]
+        upstream = self.getUpstreamRepos(projects)
+
+        self.create_branch('org/project2', 'stable/havana')
+        files = {'README': 'tagged readme'}
+        self.addCommitToRepo('org/project2', 'tagged commit',
+                             files, branch='stable/havana', tag='test-tag')
+
+        A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
+        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+        self.waitUntilSettled()
+
+        states = [
+            {p1: dict(present=[A], branch='master'),
+             p2: dict(commit=str(upstream[p2].commit('test-tag')),
+                      absent=[A]),
+             },
+        ]
+
+        self.assertBuildStates(states, projects)
+
 
 class TestAnsibleJob(ZuulTestCase):
     tenant_config_file = 'config/ansible/main.yaml'
diff --git a/zuul/configloader.py b/zuul/configloader.py
index 6737c7b..99f10f6 100644
--- a/zuul/configloader.py
+++ b/zuul/configloader.py
@@ -417,7 +417,8 @@
     role = vs.Any(zuul_role, galaxy_role)
 
     job_project = {vs.Required('name'): str,
-                   'override-branch': str}
+                   'override-branch': str,
+                   'override-checkout': str}
 
     secret = {vs.Required('name'): str,
               vs.Required('secret'): str}
@@ -452,6 +453,7 @@
                       'dependencies': to_list(str),
                       'allowed-projects': to_list(str),
                       'override-branch': str,
+                      'override-checkout': str,
                       'description': str,
                       'post-review': bool}
 
@@ -474,6 +476,7 @@
         'failure-url',
         'success-url',
         'override-branch',
+        'override-checkout',
     ]
 
     @staticmethod
@@ -633,14 +636,18 @@
                 if isinstance(project, dict):
                     project_name = project['name']
                     project_override_branch = project.get('override-branch')
+                    project_override_checkout = project.get(
+                        'override-checkout')
                 else:
                     project_name = project
                     project_override_branch = None
+                    project_override_checkout = None
                 (trusted, project) = tenant.getProject(project_name)
                 if project is None:
                     raise Exception("Unknown project %s" % (project_name,))
                 job_project = model.JobProject(project_name,
-                                               project_override_branch)
+                                               project_override_branch,
+                                               project_override_checkout)
                 new_projects[project_name] = job_project
             job.required_projects = new_projects
 
diff --git a/zuul/executor/client.py b/zuul/executor/client.py
index cfd652a..fba472f 100644
--- a/zuul/executor/client.py
+++ b/zuul/executor/client.py
@@ -196,6 +196,7 @@
         else:
             params['branch'] = None
         params['override_branch'] = job.override_branch
+        params['override_checkout'] = job.override_checkout
         params['repo_state'] = item.current_build_set.repo_state
 
         if job.name != 'noop':
@@ -216,7 +217,8 @@
         projects = set()
         required_projects = set()
 
-        def make_project_dict(project, override_branch=None):
+        def make_project_dict(project, override_branch=None,
+                              override_checkout=None):
             project_config = item.layout.project_configs.get(
                 project.canonical_name, None)
             if project_config:
@@ -228,6 +230,7 @@
                         name=project.name,
                         canonical_name=project.canonical_name,
                         override_branch=override_branch,
+                        override_checkout=override_checkout,
                         default_branch=project_default_branch)
 
         if job.required_projects:
@@ -239,7 +242,8 @@
                                     (job_project.project_name,))
                 params['projects'].append(
                     make_project_dict(project,
-                                      job_project.override_branch))
+                                      job_project.override_branch,
+                                      job_project.override_checkout))
                 projects.add(project)
                 required_projects.add(project)
         for change in dependent_changes:
diff --git a/zuul/executor/server.py b/zuul/executor/server.py
index 2951043..469d6f3 100644
--- a/zuul/executor/server.py
+++ b/zuul/executor/server.py
@@ -681,7 +681,9 @@
                                 ref,
                                 args['branch'],
                                 args['override_branch'],
+                                args['override_checkout'],
                                 project['override_branch'],
+                                project['override_checkout'],
                                 project['default_branch'])
 
         # Delete the origin remote from each repo we set up since
@@ -757,22 +759,32 @@
         return True
 
     def checkoutBranch(self, repo, project_name, ref, zuul_branch,
-                       job_branch, project_override_branch,
+                       job_override_branch, job_override_checkout,
+                       project_override_branch, project_override_checkout,
                        project_default_branch):
         branches = repo.getBranches()
+        refs = [r.name for r in repo.getRefs()]
         if project_override_branch in branches:
             self.log.info("Checking out %s project override branch %s",
                           project_name, project_override_branch)
-            repo.checkoutLocalBranch(project_override_branch)
-        elif job_branch in branches:
-            self.log.info("Checking out %s job branch %s",
-                          project_name, job_branch)
-            repo.checkoutLocalBranch(job_branch)
+            repo.checkout(project_override_branch)
+        if project_override_checkout in refs:
+            self.log.info("Checking out %s project override ref %s",
+                          project_name, project_override_checkout)
+            repo.checkout(project_override_checkout)
+        elif job_override_branch in branches:
+            self.log.info("Checking out %s job override branch %s",
+                          project_name, job_override_branch)
+            repo.checkout(job_override_branch)
+        elif job_override_checkout in refs:
+            self.log.info("Checking out %s job override ref %s",
+                          project_name, job_override_checkout)
+            repo.checkout(job_override_checkout)
         elif ref and ref.startswith('refs/heads/'):
             b = ref[len('refs/heads/'):]
             self.log.info("Checking out %s branch ref %s",
                           project_name, b)
-            repo.checkoutLocalBranch(b)
+            repo.checkout(b)
         elif ref and ref.startswith('refs/tags/'):
             t = ref[len('refs/tags/'):]
             self.log.info("Checking out %s tag ref %s",
@@ -781,11 +793,11 @@
         elif zuul_branch and zuul_branch in branches:
             self.log.info("Checking out %s zuul branch %s",
                           project_name, zuul_branch)
-            repo.checkoutLocalBranch(zuul_branch)
+            repo.checkout(zuul_branch)
         elif project_default_branch in branches:
             self.log.info("Checking out %s project default branch %s",
                           project_name, project_default_branch)
-            repo.checkoutLocalBranch(project_default_branch)
+            repo.checkout(project_default_branch)
         else:
             raise ExecutorError("Project %s does not have the "
                                 "default branch %s" %
diff --git a/zuul/merger/merger.py b/zuul/merger/merger.py
index 035d1d0..06ec4b2 100644
--- a/zuul/merger/merger.py
+++ b/zuul/merger/merger.py
@@ -176,6 +176,8 @@
         return branch in origin.refs
 
     def getBranches(self):
+        # TODO(jeblair): deprecate with override-branch; replaced by
+        # getRefs().
         repo = self.createRepoObject()
         return [x.name for x in repo.heads]
 
@@ -386,7 +388,7 @@
         self.log.info("Checking out %s/%s branch %s",
                       connection_name, project_name, branch)
         repo = self.getRepo(connection_name, project_name)
-        repo.checkoutLocalBranch(branch)
+        repo.checkout(branch)
 
     def _saveRepoState(self, connection_name, project_name, repo,
                        repo_state, recent):
diff --git a/zuul/model.py b/zuul/model.py
index cf63f64..b027c53 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -845,6 +845,7 @@
             required_projects={},
             allowed_projects=None,
             override_branch=None,
+            override_checkout=None,
             post_review=None,
         )
 
@@ -1073,9 +1074,11 @@
 class JobProject(object):
     """ A reference to a project from a job. """
 
-    def __init__(self, project_name, override_branch=None):
+    def __init__(self, project_name, override_branch=None,
+                 override_checkout=None):
         self.project_name = project_name
         self.override_branch = override_branch
+        self.override_checkout = override_checkout
 
 
 class JobList(object):