You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by je...@apache.org on 2015/02/19 17:25:49 UTC

[1/3] allura git commit: [#7830] ticket:729 One-click merge for git

Repository: allura
Updated Branches:
  refs/heads/ib/7830 [created] e37f9437e


[#7830] ticket:729 One-click merge for git


Project: http://git-wip-us.apache.org/repos/asf/allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/allura/commit/4d38103c
Tree: http://git-wip-us.apache.org/repos/asf/allura/tree/4d38103c
Diff: http://git-wip-us.apache.org/repos/asf/allura/diff/4d38103c

Branch: refs/heads/ib/7830
Commit: 4d38103cb7096a0265e930b250924d57c2a39003
Parents: a3620b4
Author: Igor Bondarenko <je...@gmail.com>
Authored: Wed Feb 18 17:00:27 2015 +0000
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Thu Feb 19 10:43:36 2015 +0000

----------------------------------------------------------------------
 Allura/allura/controllers/repository.py         | 13 ++++++++
 Allura/allura/model/repository.py               | 24 +++++++++++++++
 Allura/allura/templates/repo/merge_request.html | 26 ++++++++++++++++
 ForgeGit/forgegit/model/git_repo.py             | 32 ++++++++++++++++++++
 4 files changed, 95 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/4d38103c/Allura/allura/controllers/repository.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/repository.py b/Allura/allura/controllers/repository.py
index 1751839..a2f888d 100644
--- a/Allura/allura/controllers/repository.py
+++ b/Allura/allura/controllers/repository.py
@@ -439,6 +439,19 @@ class MergeRequestController(object):
             self.req.status = status
         redirect('.')
 
+    @expose()
+    @require_post()
+    def merge(self):
+        require_access(c.app, 'write')
+        if self.req.status != 'open' or not self.req.can_merge():
+            raise exc.HTTPNotFound
+        ok = self.req.merge()
+        if ok:
+            flash('Merged successfully', 'ok')
+        else:
+            flash('Merge failed. Please, merge manually', 'error')
+        redirect(self.req.url())
+
 
 class RefsController(object):
 

http://git-wip-us.apache.org/repos/asf/allura/blob/4d38103c/Allura/allura/model/repository.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/repository.py b/Allura/allura/model/repository.py
index 0030aa5..5e4a171 100644
--- a/Allura/allura/model/repository.py
+++ b/Allura/allura/model/repository.py
@@ -805,6 +805,30 @@ class MergeRequest(VersionedArtifact, ActivityObject):
                 self.request_number, self.project.name, self.app.repo.name))
         return result
 
+    def can_merge(self):
+        if not self.app.forkable:
+            return False
+        try:
+            result = self.app.repo.can_merge(self)
+        except:
+            log.exception(
+                "Can't determine if merge request %s can be merged",
+                self.url())
+            return False
+        return result
+
+    def merge(self):
+        if not self.app.forkable:
+            return False
+        try:
+            self.app.repo.merge(self)
+        except:
+            log.exception("Can't merge merge request %s", self.url())
+            return False
+        self.status = 'merged'
+        session(self).flush(self)
+        return True
+
 
 # Basic commit information
 # One of these for each commit in the physical repo on disk. The _id is the

http://git-wip-us.apache.org/repos/asf/allura/blob/4d38103c/Allura/allura/templates/repo/merge_request.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/repo/merge_request.html b/Allura/allura/templates/repo/merge_request.html
index 3c79654..060b5b9 100644
--- a/Allura/allura/templates/repo/merge_request.html
+++ b/Allura/allura/templates/repo/merge_request.html
@@ -44,6 +44,25 @@ Merge Request #{{req.request_number}}: {{req.summary}} ({{req.status}})
 
     <div>{{g.markdown.convert(req.description)}}</div>
 
+    {% if req.status == 'open' and c.app.forkable and h.has_access(c.app, 'write')() %}
+      {% set can_merge = req.can_merge() %}
+      <div class="grid-19">
+        <form action="merge" method="POST">
+          {{ lib.csrf_token() }}
+          <input type="submit" value="Merge"{% if not can_merge %}disabled="disabled"{% endif %}>
+          {% if can_merge %}
+            <div class="merge-ok">
+              Merge request has no conflicts. You can merge automatically.
+            </div>
+          {% else %}
+            <div class="merge-conflicts">
+              Merge request has conflicts. Follow manual instructions below to merge.
+            </div>
+          {% endif %}
+        </form>
+      </div>
+    {% endif %}
+
     {{ c.log_widget.display(value=req.commits, app=downstream_app) }}
 
     <div class="grid-19"><a href="#discussion_holder">Discuss</a></div>
@@ -87,3 +106,10 @@ Merge Request #{{req.request_number}}: {{req.summary}} ({{req.status}})
         count=count)}}
   </div>
 {% endblock %}
+
+{% block extra_css %}
+<style type="text/css">
+  .merge-ok { color: green; }
+  .merge-conflicts { color: red; }
+</style>
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/allura/blob/4d38103c/ForgeGit/forgegit/model/git_repo.py
----------------------------------------------------------------------
diff --git a/ForgeGit/forgegit/model/git_repo.py b/ForgeGit/forgegit/model/git_repo.py
index 7fda67a..831da5b 100644
--- a/ForgeGit/forgegit/model/git_repo.py
+++ b/ForgeGit/forgegit/model/git_repo.py
@@ -19,6 +19,7 @@ import os
 import shutil
 import string
 import logging
+import tempfile
 from datetime import datetime
 
 import tg
@@ -95,6 +96,37 @@ class Repository(M.Repository):
             merge_request.downstream.commit_id,
         )
 
+    def can_merge(self, mr):
+        """
+        Given merge request `mr` determine if it can be merged w/o conflicts.
+        """
+        g = self._impl._git.git
+        # http://stackoverflow.com/a/6283843
+        # fetch source branch
+        g.fetch(mr.downstream_repo_url, mr.source_branch)
+        # find merge base
+        merge_base = g.merge_base(mr.downstream.commit_id, mr.target_branch)
+        # print out merge result, but don't actually touch anything
+        merge_tree = g.merge_tree(
+            merge_base, mr.target_branch, mr.downstream.commit_id)
+        return '+<<<<<<<' not in merge_tree
+
+    def merge(self, mr):
+        g = self._impl._git.git
+        # can't merge in bare repo, so need to clone
+        tmp_path = tempfile.mkdtemp()
+        tmp_repo = git.Repo.clone_from(
+            self.clone_url('rw'),
+            to_path=tmp_path,
+            bare=False)
+        tmp_repo = GitImplementation(Object(full_fs_path=tmp_path))._git
+        tmp_repo.git.fetch('origin', mr.target_branch)
+        tmp_repo.git.checkout(mr.target_branch)
+        tmp_repo.git.fetch(mr.downstream_repo_url, mr.source_branch)
+        tmp_repo.git.merge(mr.downstream.commit_id)
+        tmp_repo.git.push('origin', mr.target_branch)
+        shutil.rmtree(tmp_path, ignore_errors=True)
+
     def rev_to_commit_id(self, rev):
         return self._impl.rev_parse(rev).hexsha
 


[2/3] allura git commit: [#7830] ticket:729 Move actual merge to background task

Posted by je...@apache.org.
[#7830] ticket:729 Move actual merge to background task


Project: http://git-wip-us.apache.org/repos/asf/allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/allura/commit/8263d331
Tree: http://git-wip-us.apache.org/repos/asf/allura/tree/8263d331
Diff: http://git-wip-us.apache.org/repos/asf/allura/diff/8263d331

Branch: refs/heads/ib/7830
Commit: 8263d3313fe42689fa2c35c49021549e7b44ef63
Parents: 4d38103
Author: Igor Bondarenko <je...@gmail.com>
Authored: Thu Feb 19 12:14:52 2015 +0000
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Thu Feb 19 13:53:00 2015 +0000

----------------------------------------------------------------------
 Allura/allura/controllers/repository.py         | 11 +--
 Allura/allura/model/repository.py               | 27 ++++---
 Allura/allura/tasks/repo_tasks.py               | 15 ++++
 Allura/allura/templates/repo/merge_request.html | 82 +++++++++++++++++---
 4 files changed, 107 insertions(+), 28 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/8263d331/Allura/allura/controllers/repository.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/repository.py b/Allura/allura/controllers/repository.py
index a2f888d..ceb9035 100644
--- a/Allura/allura/controllers/repository.py
+++ b/Allura/allura/controllers/repository.py
@@ -364,6 +364,7 @@ class MergeRequestController(object):
         return dict(
             downstream_app=downstream_app,
             req=self.req,
+            status=self.req.merge_task_status(),
             page=page,
             limit=limit,
             count=self.req.discussion_thread.post_count)
@@ -445,13 +446,13 @@ class MergeRequestController(object):
         require_access(c.app, 'write')
         if self.req.status != 'open' or not self.req.can_merge():
             raise exc.HTTPNotFound
-        ok = self.req.merge()
-        if ok:
-            flash('Merged successfully', 'ok')
-        else:
-            flash('Merge failed. Please, merge manually', 'error')
+        self.req.merge()
         redirect(self.req.url())
 
+    @expose('json:')
+    def merge_task_status(self):
+        return {'status': self.req.merge_task_status()}
+
 
 class RefsController(object):
 

http://git-wip-us.apache.org/repos/asf/allura/blob/8263d331/Allura/allura/model/repository.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/repository.py b/Allura/allura/model/repository.py
index 5e4a171..d866598 100644
--- a/Allura/allura/model/repository.py
+++ b/Allura/allura/model/repository.py
@@ -23,7 +23,7 @@ import string
 import re
 from subprocess import Popen, PIPE
 from hashlib import sha1
-from datetime import datetime
+from datetime import datetime, timedelta
 from time import time
 from collections import defaultdict, OrderedDict
 from urlparse import urljoin
@@ -818,16 +818,21 @@ class MergeRequest(VersionedArtifact, ActivityObject):
         return result
 
     def merge(self):
-        if not self.app.forkable:
-            return False
-        try:
-            self.app.repo.merge(self)
-        except:
-            log.exception("Can't merge merge request %s", self.url())
-            return False
-        self.status = 'merged'
-        session(self).flush(self)
-        return True
+        in_progress = self.merge_task_status() in ['ready', 'busy']
+        if self.app.forkable and not in_progress:
+            from allura.tasks import repo_tasks
+            repo_tasks.merge.post(self._id)
+
+    def merge_task_status(self):
+        task = MonQTask.query.find({
+            'state': {'$in': ['busy', 'complete', 'error', 'ready']},  # needed to use index
+            'task_name': 'allura.tasks.repo_tasks.merge',
+            'args': [self._id],
+            'time_queue': {'$gt': datetime.utcnow() - timedelta(days=1)}, # constrain on index further
+        }).sort('_id', -1).limit(1).first()
+        if task:
+            return task.state
+        return None
 
 
 # Basic commit information

http://git-wip-us.apache.org/repos/asf/allura/blob/8263d331/Allura/allura/tasks/repo_tasks.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tasks/repo_tasks.py b/Allura/allura/tasks/repo_tasks.py
index 61cac83..e50f9d2 100644
--- a/Allura/allura/tasks/repo_tasks.py
+++ b/Allura/allura/tasks/repo_tasks.py
@@ -20,6 +20,7 @@ import logging
 import traceback
 
 from pylons import tmpl_context as c, app_globals as g
+from ming.odm import session
 
 from allura.lib.decorators import task
 from allura.lib.repository import RepositoryApp
@@ -152,3 +153,17 @@ def tarball(revision, path):
         log.warn(
             'Skipped creation of snapshot: %s:%s because revision is not specified' %
             (c.project.shortname, c.app.config.options.mount_point))
+
+
+@task
+def merge(merge_request_id):
+    from allura import model as M
+    log = logging.getLogger(__name__)
+    mr = M.MergeRequest.query.get(_id=merge_request_id)
+    try:
+        mr.app.repo.merge(mr)
+    except:
+        log.exception("Can't merge merge request %s", mr.url())
+        return
+    mr.status = 'merged'
+    session(mr).flush(mr)

http://git-wip-us.apache.org/repos/asf/allura/blob/8263d331/Allura/allura/templates/repo/merge_request.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/repo/merge_request.html b/Allura/allura/templates/repo/merge_request.html
index 060b5b9..1520270 100644
--- a/Allura/allura/templates/repo/merge_request.html
+++ b/Allura/allura/templates/repo/merge_request.html
@@ -33,6 +33,19 @@ Merge Request #{{req.request_number}}: {{req.summary}} ({{req.status}})
 {% endblock %}
 
 {% block content %}
+  <div class="grid-19">
+    <div id="task_status">
+      {% if status == 'complete' %}
+        <h2 class="complete">Merged</h2>
+      {% else %}
+        <img src="{{g.forge_static('images/spinner.gif')}}" class="spinner" style="display:none"/>
+        <h2 class="busy ready">Merging...</h2>
+        <h2 class="complete">Merged</h2>
+        <h2 class="fail">Something went wrong. Please, merge manually</h2>
+      {% endif %}
+    </div>
+  </div>
+
   {% if req.downstream_repo %}
     <p>
       <a href="{{req.creator_url}}">{{req.creator_name}}</a>
@@ -49,7 +62,7 @@ Merge Request #{{req.request_number}}: {{req.summary}} ({{req.status}})
       <div class="grid-19">
         <form action="merge" method="POST">
           {{ lib.csrf_token() }}
-          <input type="submit" value="Merge"{% if not can_merge %}disabled="disabled"{% endif %}>
+          <input type="submit" value="Merge"{% if not can_merge or status in ('ready', 'busy') %}disabled="disabled"{% endif %}>
           {% if can_merge %}
             <div class="merge-ok">
               Merge request has no conflicts. You can merge automatically.
@@ -68,14 +81,16 @@ Merge Request #{{req.request_number}}: {{req.summary}} ({{req.status}})
     <div class="grid-19"><a href="#discussion_holder">Discuss</a></div>
 
     {% if h.has_access(c.app, 'write')() %}
-       <div class="grid-19">To merge the commits, please execute the following commands in your working
-         copy: </div>
-       <div class="grid-19"><textarea
-          style="width:80%; height:60px;"
-          readonly
-          >{{ c.app.repo.merge_command(req) | safe }}</textarea></div>
-      {{ c.mr_dispose_form.display(action="save", value=dict(status=req.status)) }}
-       <br style="clear:both">
+      <div class="grid-19">
+        To merge the commits, please execute the following commands in your working copy:
+      </div>
+      <div class="grid-19">
+        <textarea style="width:80%; height:60px;" readonly>{{ c.app.repo.merge_command(req) | safe }}</textarea>
+      </div>
+      {% if status not in ('ready', 'busy') %}
+        {{ c.mr_dispose_form.display(action="save", value=dict(status=req.status)) }}
+        <br style="clear:both">
+      {% endif %}
     {% endif %}
   {% else %}
     <p>
@@ -83,12 +98,10 @@ Merge Request #{{req.request_number}}: {{req.summary}} ({{req.status}})
       <a href="{{req.creator_url}}">{{req.creator_name}}</a>
       is deleted
     </p>
-
     <div>{{g.markdown.convert(req.description)}}</div>
-
     {% if h.has_access(c.app, 'write')() %}
       {{ c.mr_dispose_form.display(action="save", value=dict(status=req.status)) }}
-       <br style="clear:both">
+      <br style="clear:both">
     {% endif %}
   {% endif %}
 
@@ -111,5 +124,50 @@ Merge Request #{{req.request_number}}: {{req.summary}} ({{req.status}})
 <style type="text/css">
   .merge-ok { color: green; }
   .merge-conflicts { color: red; }
+
+  #task_status { margin: 0 10px; }
+  #task_status h2 { display: none; }
+  #task_status .{{ status }} { display: inline-block; }
+  #task_status h2.complete { color: #C6D880; }
+  #task_status h2.busy, #task_status h2.busy { color: #003565; }
+  #task_status h2.fail { color: #f33; }
 </style>
 {% endblock %}
+
+{% block extra_js %}
+{{ super() }}
+<script type="text/javascript">
+$(function() {
+    {% if status in ('ready', 'busy') %}
+        $('.spinner').show();
+        var delay = 500;
+        function check_status() {
+          $.get("{{request.path.rstrip('/') + '/merge_task_status'}}", function(data) {
+                if (data.status === 'complete') {
+                    $('.spinner').hide();
+                    $('#task_status h2').hide();
+                    $('#task_status h2.complete').show();
+                    location.reload();
+                } else {
+                    if (data.status === 'ready' || data.status === 'busy') {
+                        // keep waiting
+                        $('#task_status h2').hide();
+                        $('#task_status h2.busy').show();
+                    } else {
+                        // something went wrong
+                        $('.spinner').hide();
+                        $('#task_status h2').hide();
+                        $('#task_status h2.fail').show();
+                    }
+                    if (delay < 60000){
+                        delay = delay * 2;
+                    }
+                    window.setTimeout(check_status, delay);
+                }
+            });
+        }
+        window.setTimeout(check_status, delay);
+    {% endif %}
+});
+</script>
+{% endblock %}


[3/3] allura git commit: [#7830] ticket:729 Add tests

Posted by je...@apache.org.
[#7830] ticket:729 Add tests


Project: http://git-wip-us.apache.org/repos/asf/allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/allura/commit/e37f9437
Tree: http://git-wip-us.apache.org/repos/asf/allura/tree/e37f9437
Diff: http://git-wip-us.apache.org/repos/asf/allura/diff/e37f9437

Branch: refs/heads/ib/7830
Commit: e37f9437ea40922ba31cefd6a4096b43c9a7cbb4
Parents: 8263d33
Author: Igor Bondarenko <je...@gmail.com>
Authored: Thu Feb 19 15:53:48 2015 +0000
Committer: Igor Bondarenko <je...@gmail.com>
Committed: Thu Feb 19 15:53:48 2015 +0000

----------------------------------------------------------------------
 Allura/allura/tests/model/test_repo.py          | 38 ++++++++++++++++
 Allura/allura/tests/test_tasks.py               | 11 +++++
 .../forgegit/tests/model/test_repository.py     | 46 ++++++++++++++++++++
 3 files changed, 95 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/e37f9437/Allura/allura/tests/model/test_repo.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/model/test_repo.py b/Allura/allura/tests/model/test_repo.py
index 9e2af07..669f940 100644
--- a/Allura/allura/tests/model/test_repo.py
+++ b/Allura/allura/tests/model/test_repo.py
@@ -713,3 +713,41 @@ class TestModelCache(unittest.TestCase):
         session.assert_called_once_with(tree1)
         session.return_value.flush.assert_called_once_with(tree1)
         session.return_value.expunge.assert_called_once_with(tree1)
+
+
+class TestMergeRequest(object):
+
+    def setUp(self):
+        setup_basic_test()
+        setup_global_objects()
+        self.mr = M.MergeRequest(app_config=mock.Mock(_id=ObjectId()))
+        self.mr.app = mock.Mock(forkable=True)
+
+    def test_can_merge(self):
+        assert_equal(self.mr.can_merge(),
+                     self.mr.app.repo.can_merge.return_value)
+        self.mr.app.repo.can_merge.assert_called_once_with(self.mr)
+
+        self.mr.app.reset_mock()
+        self.mr.app.forkable = False
+        assert_equal(self.mr.can_merge(), False)
+        assert_equal(self.mr.app.repo.can_merge.called, False)
+
+    @mock.patch('allura.tasks.repo_tasks.merge', autospec=True)
+    def test_merge(self, merge_task):
+        self.mr.merge_task_status = lambda: None
+        self.mr.merge()
+        merge_task.post.assert_called_once_with(self.mr._id)
+
+        merge_task.reset_mock()
+        self.mr.merge_task_status = lambda: 'ready'
+        self.mr.merge()
+        assert_equal(merge_task.post.called, False)
+
+    def test_merge_task_status(self):
+        from allura.tasks import repo_tasks
+        assert_equal(self.mr.merge_task_status(), None)
+        repo_tasks.merge.post(self.mr._id)
+        assert_equal(self.mr.merge_task_status(), 'ready')
+        M.MonQTask.run_ready()
+        assert_equal(self.mr.merge_task_status(), 'complete')

http://git-wip-us.apache.org/repos/asf/allura/blob/e37f9437/Allura/allura/tests/test_tasks.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/test_tasks.py b/Allura/allura/tests/test_tasks.py
index 13d5c17..d822229 100644
--- a/Allura/allura/tests/test_tasks.py
+++ b/Allura/allura/tests/test_tasks.py
@@ -64,6 +64,17 @@ class TestRepoTasks(unittest.TestCase):
         assert_equal(post_event.call_args[0][2], None)
         # ignore args[3] which is a traceback string
 
+    @mock.patch('allura.tasks.repo_tasks.session', autospec=True)
+    @mock.patch.object(M, 'MergeRequest', autospec=True)
+    def test_merge(self, MR, session):
+        mr = mock.Mock(_id='_id')
+        MR.query.get.return_value = mr
+        repo_tasks.merge(mr._id)
+        mr.app.repo.merge.assert_called_once_with(mr)
+        assert_equal(mr.status, 'merged')
+        session.assert_called_once_with(mr)
+        session.return_value.flush.assert_called_once_with(mr)
+
 
 class TestEventTasks(unittest.TestCase):
 

http://git-wip-us.apache.org/repos/asf/allura/blob/e37f9437/ForgeGit/forgegit/tests/model/test_repository.py
----------------------------------------------------------------------
diff --git a/ForgeGit/forgegit/tests/model/test_repository.py b/ForgeGit/forgegit/tests/model/test_repository.py
index 231e479..d79fc7b 100644
--- a/ForgeGit/forgegit/tests/model/test_repository.py
+++ b/ForgeGit/forgegit/tests/model/test_repository.py
@@ -592,6 +592,52 @@ class TestGitRepo(unittest.TestCase, RepoImplTestBase):
         }
         assert_equals(payload, expected_payload)
 
+    def test_can_merge(self):
+        mr = mock.Mock(downstream_repo_url='downstream-url',
+                       source_branch='source-branch',
+                       target_branch='target-branch',
+                       downstream=mock.Mock(commit_id='cid'))
+        git = mock.Mock()
+        git.merge_tree.return_value = 'clean merge'
+        self.repo._impl._git.git = git
+        assert_equal(self.repo.can_merge(mr), True)
+        git.fetch.assert_called_once_with('downstream-url', 'source-branch')
+        git.merge_base.assert_called_once_with('cid', 'target-branch')
+        git.merge_tree.assert_called_once_with(
+            git.merge_base.return_value,
+            'target-branch',
+            'cid')
+        git.merge_tree.return_value = '+<<<<<<<'
+        assert_equal(self.repo.can_merge(mr), False)
+
+    @mock.patch('forgegit.model.git_repo.tempfile', autospec=True)
+    @mock.patch('forgegit.model.git_repo.git', autospec=True)
+    @mock.patch('forgegit.model.git_repo.GitImplementation', autospec=True)
+    @mock.patch('forgegit.model.git_repo.shutil', autospec=True)
+    def test_merge(self, shutil, GitImplementation, git, tempfile):
+        mr = mock.Mock(downstream_repo_url='downstream-url',
+                       source_branch='source-branch',
+                       target_branch='target-branch',
+                       downstream=mock.Mock(commit_id='cid'))
+        _git = mock.Mock()
+        self.repo._impl._git.git = _git
+        self.repo.merge(mr)
+        git.Repo.clone_from.assert_called_once_with(
+            self.repo.clone_url('rw'),
+            to_path=tempfile.mkdtemp.return_value,
+            bare=False)
+        tmp_repo = GitImplementation.return_value._git
+        assert_equal(
+            tmp_repo.git.fetch.call_args_list,
+            [mock.call('origin', 'target-branch'),
+             mock.call('downstream-url', 'source-branch')])
+        tmp_repo.git.checkout.assert_called_once_with('target-branch')
+        tmp_repo.git.merge.assert_called_once_with('cid')
+        tmp_repo.git.push.assert_called_once_with('origin', 'target-branch')
+        shutil.rmtree.assert_called_once_with(
+            tempfile.mkdtemp.return_value,
+            ignore_errors=True)
+
 
 class TestGitImplementation(unittest.TestCase):