Add merging capability.

Add git repo management and merging.  When collecting changes to
be tested together, merge or cherry-pick those changes into the
zuul-managed repos, and create a unique ref for that configuration.
Pass the ref to Jenkins instead of the string description of the
changes, so that Jenkins only needs to checkout that one ref.
This moves the complexity of merging and managing multiple commits
out of Jenkins and into Zuul.

The GERRIT_CHANGES variable is deprecated (along with the rest of
the GERRIT_* variables) and will be removed in a future patch
(which will contain a documentation update).

Change-Id: I126c9030223c07a30f7092e2273ebd7605d9f3df
Reviewed-on: https://review.openstack.org/11349
Reviewed-by: Monty Taylor <mordred@inaugust.com>
Reviewed-by: Clark Boylan <clark.boylan@gmail.com>
Approved: James E. Blair <corvus@inaugust.com>
Tested-by: Jenkins
diff --git a/tests/fixtures/layout.yaml b/tests/fixtures/layout.yaml
index 9c4b4d7..35eb737 100644
--- a/tests/fixtures/layout.yaml
+++ b/tests/fixtures/layout.yaml
@@ -42,6 +42,7 @@
 
 projects:
   - name: org/project
+    merge-mode: cherry-pick
     check:
       - project-merge:
         - project-test1
diff --git a/tests/fixtures/zuul.conf b/tests/fixtures/zuul.conf
index b66b489..916cddd 100644
--- a/tests/fixtures/zuul.conf
+++ b/tests/fixtures/zuul.conf
@@ -10,3 +10,4 @@
 
 [zuul]
 layout_config=layout.yaml
+git_dir=/tmp/zuul-test/git
diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py
index fae05d0..fc6d4a4 100644
--- a/tests/test_scheduler.py
+++ b/tests/test_scheduler.py
@@ -28,6 +28,8 @@
 import re
 import urllib2
 import urlparse
+import shutil
+import git
 
 import zuul
 import zuul.scheduler
@@ -49,6 +51,75 @@
     return hashlib.sha1(str(random.random())).hexdigest()
 
 
+class ChangeReference(git.Reference):
+    _common_path_default = "refs/changes"
+    _points_to_commits_only = True
+
+
+def init_repo(project):
+    parts = project.split('/')
+    path = os.path.join("/tmp/zuul-test/upstream", *parts[:-1])
+    if not os.path.exists(path):
+        os.makedirs(path)
+    path = os.path.join("/tmp/zuul-test/upstream", project)
+    repo = git.Repo.init(path)
+
+    fn = os.path.join(path, 'README')
+    f = open(fn, 'w')
+    f.write("test\n")
+    f.close()
+    repo.index.add([fn])
+    repo.index.commit('initial commit')
+    repo.create_head('master')
+    repo.create_tag('init')
+
+
+def add_fake_change_to_repo(project, branch, change_num, patchset, msg):
+    path = os.path.join("/tmp/zuul-test/upstream", project)
+    repo = git.Repo(path)
+    ref = ChangeReference.create(repo, '1/%s/%s' % (change_num,
+                                                    patchset),
+                                 'refs/tags/init')
+    repo.head.reference = ref
+    repo.head.reset(index=True, working_tree=True)
+    repo.git.clean('-x', '-f', '-d')
+
+    path = os.path.join("/tmp/zuul-test/upstream", project)
+    fn = os.path.join(path, '%s-%s' % (branch, change_num))
+    f = open(fn, 'w')
+    f.write("test\n")
+    f.close()
+    repo.index.add([fn])
+    repo.index.commit(msg)
+
+
+def ref_has_change(ref, change):
+    path = os.path.join("/tmp/zuul-test/git", change.project)
+    repo = git.Repo(path)
+    for commit in repo.iter_commits(ref):
+        if commit.message.strip() == ('%s-1' % change.subject):
+            return True
+    return False
+
+
+def job_has_changes(*args):
+    job = args[0]
+    commits = args[1:]
+    project = job.parameters['ZUUL_PROJECT']
+    path = os.path.join("/tmp/zuul-test/git", project)
+    repo = git.Repo(path)
+    ref = job.parameters['ZUUL_REF']
+    repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
+    commit_messages = ['%s-1' % commit.subject for commit in commits]
+    print 'checking that job %s has changes:' % ref
+    print '  commit messages:', commit_messages
+    print '  repo messages  :', repo_messages
+    for msg in commit_messages:
+        if msg not in repo_messages:
+            return False
+    return True
+
+
 class FakeChange(object):
     categories = {'APRV': ('Approved', -1, 1),
                   'CRVW': ('Code-Review', -2, 2),
@@ -106,6 +177,9 @@
         self.data['currentPatchSet'] = d
         self.patchsets.append(d)
         self.data['submitRecords'] = self.getSubmitRecords()
+        add_fake_change_to_repo(self.project, self.branch,
+                                self.number, self.latest_patchset,
+                                self.subject + '-' + str(self.latest_patchset))
 
     def addApproval(self, category, value):
         approval = {'description': self.categories[category][0],
@@ -321,7 +395,7 @@
         result = 'SUCCESS'
         if self.jenkins.fakeShouldFailTest(
             self.name,
-            self.parameters['GERRIT_CHANGES']):
+            self.parameters['ZUUL_REF']):
             result = 'FAILURE'
         if self.aborted:
             result = 'ABORTED'
@@ -386,10 +460,10 @@
         l.append(change)
         self.fail_tests[name] = l
 
-    def fakeShouldFailTest(self, name, changes):
+    def fakeShouldFailTest(self, name, ref):
         l = self.fail_tests.get(name, [])
         for change in l:
-            if change in changes:
+            if ref_has_change(ref, change):
                 return True
         return False
 
@@ -476,10 +550,25 @@
         return ret
 
 
+class FakeGerritTrigger(zuul.trigger.gerrit.Gerrit):
+    def getGitUrl(self, project):
+        return "/tmp/zuul-test/upstream/%s" % project
+
+
 class testScheduler(unittest.TestCase):
     log = logging.getLogger("zuul.test")
 
     def setUp(self):
+        if os.path.exists("/tmp/zuul-test"):
+            shutil.rmtree("/tmp/zuul-test")
+        os.makedirs("/tmp/zuul-test")
+        os.makedirs("/tmp/zuul-test/upstream")
+        os.makedirs("/tmp/zuul-test/git")
+
+        # For each project in config:
+        init_repo("org/project")
+        init_repo("org/project1")
+        init_repo("org/project2")
         self.config = CONFIG
         self.sched = zuul.scheduler.Scheduler()
 
@@ -503,7 +592,7 @@
 
         zuul.lib.gerrit.Gerrit = FakeGerrit
 
-        self.gerrit = zuul.trigger.gerrit.Gerrit(self.config, self.sched)
+        self.gerrit = FakeGerritTrigger(self.config, self.sched)
         self.gerrit.replication_timeout = 1.5
         self.gerrit.replication_retry_interval = 0.5
         self.fake_gerrit = self.gerrit.gerrit
@@ -520,6 +609,7 @@
         self.gerrit.stop()
         self.sched.stop()
         self.sched.join()
+        #shutil.rmtree("/tmp/zuul-test")
 
     def waitUntilSettled(self):
         self.log.debug("Waiting until settled...")
@@ -582,77 +672,51 @@
         jobs = self.fake_jenkins.all_jobs
         assert len(jobs) == 1
         assert jobs[0].name == 'project-merge'
-        assert (jobs[0].parameters['GERRIT_CHANGES'] ==
-                'org/project:master:refs/changes/1/1/1')
+        assert job_has_changes(jobs[0], A)
 
         self.fake_jenkins.fakeRelease('.*-merge')
         self.waitUntilSettled()
         assert len(jobs) == 3
         assert jobs[0].name == 'project-test1'
-        assert (jobs[0].parameters['GERRIT_CHANGES'] ==
-                'org/project:master:refs/changes/1/1/1')
+        assert job_has_changes(jobs[0], A)
         assert jobs[1].name == 'project-test2'
-        assert (jobs[1].parameters['GERRIT_CHANGES'] ==
-                'org/project:master:refs/changes/1/1/1')
+        assert job_has_changes(jobs[1], A)
         assert jobs[2].name == 'project-merge'
-        assert (jobs[2].parameters['GERRIT_CHANGES'] ==
-                'org/project:master:refs/changes/1/1/1^'
-                'org/project:master:refs/changes/1/2/1')
+        assert job_has_changes(jobs[2], A, B)
 
         self.fake_jenkins.fakeRelease('.*-merge')
         self.waitUntilSettled()
         assert len(jobs) == 5
         assert jobs[0].name == 'project-test1'
-        assert (jobs[0].parameters['GERRIT_CHANGES'] ==
-                'org/project:master:refs/changes/1/1/1')
+        assert job_has_changes(jobs[0], A)
         assert jobs[1].name == 'project-test2'
-        assert (jobs[1].parameters['GERRIT_CHANGES'] ==
-                'org/project:master:refs/changes/1/1/1')
+        assert job_has_changes(jobs[1], A)
 
         assert jobs[2].name == 'project-test1'
-        assert (jobs[2].parameters['GERRIT_CHANGES'] ==
-                'org/project:master:refs/changes/1/1/1^'
-                'org/project:master:refs/changes/1/2/1')
+        assert job_has_changes(jobs[2], A, B)
         assert jobs[3].name == 'project-test2'
-        assert (jobs[3].parameters['GERRIT_CHANGES'] ==
-                'org/project:master:refs/changes/1/1/1^'
-                'org/project:master:refs/changes/1/2/1')
+        assert job_has_changes(jobs[3], A, B)
 
         assert jobs[4].name == 'project-merge'
-        assert (jobs[4].parameters['GERRIT_CHANGES'] ==
-                'org/project:master:refs/changes/1/1/1^'
-                'org/project:master:refs/changes/1/2/1^'
-                'org/project:master:refs/changes/1/3/1')
+        assert job_has_changes(jobs[4], A, B, C)
 
         self.fake_jenkins.fakeRelease('.*-merge')
         self.waitUntilSettled()
         assert len(jobs) == 6
         assert jobs[0].name == 'project-test1'
-        assert (jobs[0].parameters['GERRIT_CHANGES'] ==
-                'org/project:master:refs/changes/1/1/1')
+        assert job_has_changes(jobs[0], A)
         assert jobs[1].name == 'project-test2'
-        assert (jobs[1].parameters['GERRIT_CHANGES'] ==
-                'org/project:master:refs/changes/1/1/1')
+        assert job_has_changes(jobs[1], A)
 
         assert jobs[2].name == 'project-test1'
-        assert (jobs[2].parameters['GERRIT_CHANGES'] ==
-                'org/project:master:refs/changes/1/1/1^'
-                'org/project:master:refs/changes/1/2/1')
+        assert job_has_changes(jobs[2], A, B)
         assert jobs[3].name == 'project-test2'
-        assert (jobs[3].parameters['GERRIT_CHANGES'] ==
-                'org/project:master:refs/changes/1/1/1^'
-                'org/project:master:refs/changes/1/2/1')
+        assert job_has_changes(jobs[3], A, B)
 
         assert jobs[4].name == 'project-test1'
-        assert (jobs[4].parameters['GERRIT_CHANGES'] ==
-                'org/project:master:refs/changes/1/1/1^'
-                'org/project:master:refs/changes/1/2/1^'
-                'org/project:master:refs/changes/1/3/1')
+        assert job_has_changes(jobs[4], A, B, C)
         assert jobs[5].name == 'project-test2'
-        assert (jobs[5].parameters['GERRIT_CHANGES'] ==
-                'org/project:master:refs/changes/1/1/1^'
-                'org/project:master:refs/changes/1/2/1^'
-                'org/project:master:refs/changes/1/3/1')
+        assert job_has_changes(jobs[5], A, B, C)
 
         self.fake_jenkins.hold_jobs_in_build = False
         self.fake_jenkins.fakeRelease()
@@ -678,9 +742,7 @@
         self.fake_gerrit.addEvent(A.addApproval('APRV', 1))
         self.fake_gerrit.addEvent(B.addApproval('APRV', 1))
 
-        self.fake_jenkins.fakeAddFailTest(
-            'project-test1',
-            'org/project:master:refs/changes/1/1/1')
+        self.fake_jenkins.fakeAddFailTest('project-test1', A)
 
         self.waitUntilSettled()
         jobs = self.fake_jenkins.job_history
@@ -710,11 +772,9 @@
         # There should be one merge job at the head of each queue running
         assert len(jobs) == 2
         assert jobs[0].name == 'project-merge'
-        assert (jobs[0].parameters['GERRIT_CHANGES'] ==
-                'org/project:master:refs/changes/1/1/1')
+        assert job_has_changes(jobs[0], A)
         assert jobs[1].name == 'project1-merge'
-        assert (jobs[1].parameters['GERRIT_CHANGES'] ==
-                'org/project1:master:refs/changes/1/2/1')
+        assert job_has_changes(jobs[1], B)
 
         # Release the current merge jobs
         self.fake_jenkins.fakeRelease('.*-merge')
@@ -751,9 +811,7 @@
         B.addApproval('CRVW', 2)
         C.addApproval('CRVW', 2)
 
-        self.fake_jenkins.fakeAddFailTest(
-            'project-test1',
-            'org/project:master:refs/changes/1/1/1')
+        self.fake_jenkins.fakeAddFailTest('project-test1', A)
 
         self.fake_gerrit.addEvent(A.addApproval('APRV', 1))
         self.fake_gerrit.addEvent(B.addApproval('APRV', 1))
@@ -765,8 +823,7 @@
 
         assert len(jobs) == 1
         assert jobs[0].name == 'project-merge'
-        assert (jobs[0].parameters['GERRIT_CHANGES'] ==
-                'org/project:master:refs/changes/1/1/1')
+        assert job_has_changes(jobs[0], A)
 
         self.fake_jenkins.fakeRelease('.*-merge')
         self.waitUntilSettled()
@@ -813,9 +870,7 @@
         B.addApproval('CRVW', 2)
         C.addApproval('CRVW', 2)
 
-        self.fake_jenkins.fakeAddFailTest(
-            'project-test1',
-            'org/project:master:refs/changes/1/1/1')
+        self.fake_jenkins.fakeAddFailTest('project-test1', A)
 
         self.fake_gerrit.addEvent(A.addApproval('APRV', 1))
         self.fake_gerrit.addEvent(B.addApproval('APRV', 1))
@@ -829,8 +884,7 @@
         assert len(jobs) == 1
         assert len(queue) == 1
         assert jobs[0].name == 'project-merge'
-        assert (jobs[0].parameters['GERRIT_CHANGES'] ==
-                'org/project:master:refs/changes/1/1/1')
+        assert job_has_changes(jobs[0], A)
 
         self.fake_jenkins.fakeRelease('.*-merge')
         self.waitUntilSettled()
@@ -913,7 +967,7 @@
         assert C.reported == 2
 
     def test_can_merge(self):
-        "Test that whether a change is ready to merge"
+        "Test whether a change is ready to merge"
         # TODO: move to test_gerrit (this is a unit test!)
         A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
         a = self.sched.trigger.getChange(1, 2)
@@ -929,3 +983,37 @@
         assert self.sched.trigger.canMerge(a, mgr.getSubmitAllowNeeds())
 
         return True
+
+    def test_build_configuration(self):
+        "Test that zuul merges the right commits for testing"
+        self.fake_jenkins.hold_jobs_in_queue = True
+        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+        B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+        C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
+        A.addApproval('CRVW', 2)
+        B.addApproval('CRVW', 2)
+        C.addApproval('CRVW', 2)
+        self.fake_gerrit.addEvent(A.addApproval('APRV', 1))
+        self.fake_gerrit.addEvent(B.addApproval('APRV', 1))
+        self.fake_gerrit.addEvent(C.addApproval('APRV', 1))
+        self.waitUntilSettled()
+
+        jobs = self.fake_jenkins.all_jobs
+
+        self.fake_jenkins.fakeRelease('.*-merge')
+        self.waitUntilSettled()
+        self.fake_jenkins.fakeRelease('.*-merge')
+        self.waitUntilSettled()
+        self.fake_jenkins.fakeRelease('.*-merge')
+        self.waitUntilSettled()
+        ref = jobs[-1].parameters['ZUUL_REF']
+        self.fake_jenkins.hold_jobs_in_queue = False
+        self.fake_jenkins.fakeRelease()
+
+        path = os.path.join("/tmp/zuul-test/git/org/project")
+        repo = git.Repo(path)
+        repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
+        repo_messages.reverse()
+        print '  repo messages  :', repo_messages
+        correct_messages = ['initial commit', 'A-1', 'B-1', 'C-1']
+        assert repo_messages == correct_messages
diff --git a/tools/pip-requires b/tools/pip-requires
index 9dcc275..aa1debe 100644
--- a/tools/pip-requires
+++ b/tools/pip-requires
@@ -3,3 +3,4 @@
 Paste
 webob
 paramiko
+GitPython>=0.3.2.RC1
diff --git a/zuul/launcher/jenkins.py b/zuul/launcher/jenkins.py
index a026810..a4d8968 100644
--- a/zuul/launcher/jenkins.py
+++ b/zuul/launcher/jenkins.py
@@ -24,7 +24,7 @@
 import urllib   # for extending jenkins lib
 import urllib2  # for extending jenkins lib
 import urlparse
-from uuid import uuid1
+from uuid import uuid4
 
 import jenkins
 from paste import httpserver
@@ -207,24 +207,32 @@
         self.cleanup_thread.stop()
         self.cleanup_thread.join()
 
+    #TODO: remove dependent_changes
     def launch(self, job, change, dependent_changes=[]):
         self.log.info("Launch job %s for change %s with dependent changes %s" %
                       (job, change, dependent_changes))
         dependent_changes = dependent_changes[:]
         dependent_changes.reverse()
-        uuid = str(uuid1())
+        uuid = str(uuid4().hex)
         params = dict(UUID=uuid,
-                      GERRIT_PROJECT=change.project.name)
+                      GERRIT_PROJECT=change.project.name,
+                      ZUUL_PROJECT=change.project.name)
         if hasattr(change, 'refspec'):
             changes_str = '^'.join(
                 ['%s:%s:%s' % (c.project.name, c.branch, c.refspec)
                  for c in dependent_changes + [change]])
             params['GERRIT_BRANCH'] = change.branch
+            params['ZUUL_BRANCH'] = change.branch
             params['GERRIT_CHANGES'] = changes_str
+            params['ZUUL_REF'] = 'refs/zuul/%s/%s' % (change.branch,
+                change.current_build_set.ref)
         if hasattr(change, 'ref'):
             params['GERRIT_REFNAME'] = change.ref
+            params['ZUUL_REFNAME'] = change.ref
             params['GERRIT_OLDREV'] = change.oldrev
+            params['ZUUL_OLDREV'] = change.oldrev
             params['GERRIT_NEWREV'] = change.newrev
+            params['ZUUL_NEWREV'] = change.newrev
 
         if callable(job.parameter_function):
             job.parameter_function(change, params)
diff --git a/zuul/merger.py b/zuul/merger.py
new file mode 100644
index 0000000..a1345b8
--- /dev/null
+++ b/zuul/merger.py
@@ -0,0 +1,126 @@
+# Copyright 2012 Hewlett-Packard Development Company, L.P.
+#
+# 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.
+
+import git
+import os
+import logging
+import model
+
+
+class ZuulReference(git.Reference):
+    _common_path_default = "refs/zuul"
+    _points_to_commits_only = True
+
+
+class Repo(object):
+    log = logging.getLogger("zuul.Repo")
+
+    def __init__(self, remote, local):
+        self.remote_url = remote
+        self.local_path = local
+        self._ensure_cloned()
+        self.repo = git.Repo(self.local_path)
+
+    def _ensure_cloned(self):
+        if not os.path.exists(self.local_path):
+            self.log.debug("Cloning from %s to %s" % (self.remote_url,
+                                                      self.local_path))
+            git.Repo.clone_from(self.remote_url, self.local_path)
+
+    def reset(self):
+        self.log.debug("Resetting repository %s" % self.local_path)
+        origin = self.repo.remotes.origin
+        origin.update()
+        self.repo.head.reference = origin.refs.master
+        self.repo.head.reset(index=True, working_tree=True)
+        self.repo.git.clean('-x', '-f', '-d')
+
+    def cherryPick(self, ref):
+        self.log.debug("Cherry-picking %s" % ref)
+        origin = self.repo.remotes.origin
+        origin.fetch(ref)
+        self.repo.git.cherry_pick("FETCH_HEAD")
+
+    def merge(self, ref):
+        self.log.debug("Merging %s" % ref)
+        origin = self.repo.remotes.origin
+        origin.fetch(ref)
+        self.repo.git.merge("FETCH_HEAD")
+
+    def createZuulRef(self, ref):
+        ref = ZuulReference.create(self.repo, ref, 'HEAD')
+        return ref
+
+    def setZuulRef(self, ref, commit):
+        self.repo.refs[ref].commit = commit
+
+
+class Merger(object):
+    log = logging.getLogger("zuul.Merger")
+
+    def __init__(self, working_root):
+        self.repos = {}
+        self.working_root = working_root
+        if not os.path.exists(working_root):
+            os.makedirs(working_root)
+
+    def addProject(self, project, url):
+        try:
+            path = os.path.join(self.working_root, project.name)
+            repo = Repo(url, path)
+            self.repos[project] = repo
+        except:
+            self.log.exception("Unable to initialize repo for %s" % project)
+
+    def getRepo(self, project):
+        return self.repos.get(project, None)
+
+    def mergeChanges(self, changes, target_ref=None, mode=None):
+        projects = {}
+        # Reset all repos involved in the change set
+        for change in changes:
+            branches = projects.get(change.project, [])
+            if change.branch not in branches:
+                repo = self.getRepo(change.project)
+                if not repo:
+                    self.log.error("Unable to find repo for %s" %
+                                   change.project)
+                    return False
+                try:
+                    repo.reset()
+                except:
+                    self.log.exception("Unable to reset repo %s" % repo)
+                    return False
+
+                if target_ref:
+                    repo.createZuulRef(change.branch + '/' + target_ref)
+                branches.append(change.branch)
+                projects[change.project] = branches
+
+        # Merge all the changes
+        for change in changes:
+            repo = self.getRepo(change.project)
+            try:
+                if not mode:
+                    mode = change.project.merge_mode
+                if mode == model.MERGE_IF_NECESSARY:
+                    repo.merge(change.refspec)
+                elif mode == model.CHERRY_PICK:
+                    repo.cherryPick(change.refspec)
+                repo.setZuulRef(change.branch + '/' + target_ref, 'HEAD')
+            except:
+                self.log.exception("Unable to merge %s" % change)
+                return False
+
+        return True
diff --git a/zuul/model.py b/zuul/model.py
index 8c625ab..1674082 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -14,6 +14,13 @@
 
 import re
 import time
+from uuid import uuid4
+
+
+FAST_FORWARD_ONLY = 1
+MERGE_ALWAYS = 2
+MERGE_IF_NECESSARY = 3
+CHERRY_PICK = 4
 
 
 class Pipeline(object):
@@ -327,6 +334,7 @@
 class Project(object):
     def __init__(self, name):
         self.name = name
+        self.merge_mode = MERGE_IF_NECESSARY
 
     def __str__(self):
         return self.name
@@ -424,11 +432,9 @@
         self.result = None
         self.next_build_set = None
         self.previous_build_set = None
+        self.ref = None
 
-    def addBuild(self, build):
-        self.builds[build.job.name] = build
-        build.build_set = self
-
+    def setConfiguration(self):
         # The change isn't enqueued until after it's created
         # so we don't know what the other changes ahead will be
         # until jobs start.
@@ -437,6 +443,15 @@
             while next_change:
                 self.other_changes.append(next_change)
                 next_change = next_change.change_ahead
+        if not self.ref:
+            self.ref = 'Z' + uuid4().hex
+
+    def getRef(self):
+        return self.ref
+
+    def addBuild(self, build):
+        self.builds[build.job.name] = build
+        build.build_set = self
 
     def getBuild(self, job_name):
         return self.builds.get(job_name)
diff --git a/zuul/scheduler.py b/zuul/scheduler.py
index 4e92336..5fd2126 100644
--- a/zuul/scheduler.py
+++ b/zuul/scheduler.py
@@ -20,7 +20,9 @@
 import threading
 import yaml
 
+import model
 from model import Pipeline, Job, Project, ChangeQueue, EventFilter
+import merger
 
 
 class Scheduler(threading.Thread):
@@ -139,6 +141,9 @@
         for config_project in data['projects']:
             project = Project(config_project['name'])
             self.projects[config_project['name']] = project
+            mode = config_project.get('merge-mode')
+            if mode and mode == 'cherry-pick':
+                project.merge_mode = model.CHERRY_PICK
             for pipeline in self.pipelines.values():
                 if pipeline.name in config_project:
                     job_tree = pipeline.addProject(project)
@@ -154,6 +159,15 @@
         for pipeline in self.pipelines.values():
             pipeline.manager._postConfig()
 
+        if self.config.has_option('zuul', 'git_dir'):
+            merge_root = self.config.get('zuul', 'git_dir')
+        else:
+            merge_root = '/var/lib/zuul/git'
+        self.merger = merger.Merger(merge_root)
+        for project in self.projects.values():
+            url = self.trigger.getGitUrl(project)
+            self.merger.addProject(project, url)
+
     def getJob(self, name):
         if name in self.jobs:
             return self.jobs[name]
@@ -464,6 +478,13 @@
 
     def launchJobs(self, change):
         self.log.debug("Launching jobs for change %s" % change)
+        ref = change.current_build_set.getRef()
+        if not ref:
+            change.current_build_set.setConfiguration()
+            ref = change.current_build_set.getRef()
+            self.sched.merger.mergeChanges([change], ref,
+                                           mode=model.MERGE_IF_NECESSARY)
+
         for job in self.pipeline.findJobsToRun(change):
             self.log.debug("Found job %s for change %s" % (job, change))
             try:
@@ -720,12 +741,21 @@
 
     def launchJobs(self, change):
         self.log.debug("Launching jobs for change %s" % change)
+        ref = change.current_build_set.getRef()
+        if not ref:
+            change.current_build_set.setConfiguration()
+            ref = change.current_build_set.getRef()
+            dependent_changes = self._getDependentChanges(change)
+            dependent_changes.reverse()
+            self.sched.merger.mergeChanges(dependent_changes + [change], ref)
+
+        #TODO: remove this line after GERRIT_CHANGES is gone
         dependent_changes = self._getDependentChanges(change)
         for job in self.pipeline.findJobsToRun(change):
             self.log.debug("Found job %s for change %s" % (job, change))
             try:
-                build = self.sched.launcher.launch(job,
-                                                   change,
+                #TODO: remove dependent_changes after GERRIT_CHANGES is gone
+                build = self.sched.launcher.launch(job, change,
                                                    dependent_changes)
                 self.building_jobs[build] = change
                 self.log.debug("Adding build %s of job %s to change %s" %
diff --git a/zuul/trigger/gerrit.py b/zuul/trigger/gerrit.py
index ac707fb..6542856 100644
--- a/zuul/trigger/gerrit.py
+++ b/zuul/trigger/gerrit.py
@@ -79,6 +79,7 @@
 
     def __init__(self, config, sched):
         self.sched = sched
+        self.config = config
         self.server = config.get('gerrit', 'server')
         user = config.get('gerrit', 'user')
         if config.has_option('gerrit', 'sshkey'):
@@ -270,3 +271,13 @@
                     change.needed_by_changes.append(dep)
 
         return change
+
+    def getGitUrl(self, project):
+        server = self.config.get('gerrit', 'server')
+        user = self.config.get('gerrit', 'user')
+        if self.config.has_option('gerrit', 'port'):
+            port = self.config.get('gerrit', 'port')
+        else:
+            port = 29418
+        url = 'ssh://%s@%s:%s/%s' % (user, server, port, project.name)
+        return url