[#7882] ticket:783 Option to use a tmp dir for git ops on merge request view
diff --git a/Allura/development.ini b/Allura/development.ini
index baf479d..d5a12f4 100644
--- a/Allura/development.ini
+++ b/Allura/development.ini
@@ -301,6 +301,10 @@
 scm.merge.git.disabled = false
 scm.merge.hg.disabled = false
 
+; Merge request viewing will fetch git info into existing git repositories. On
+; deployments where writes in existing git repos are unwanted, clone to tmp dir
+; can be used.
+;scm.merge_list.git.use_tmp_dir = true
 
 ; bulk_export_enabled = true
 ; If you keep bulk_export_enabled, you should set up your server to securely share bulk_export_path with users somehow
diff --git a/ForgeGit/forgegit/model/git_repo.py b/ForgeGit/forgegit/model/git_repo.py
index aeb5734..bb3b8d8 100644
--- a/ForgeGit/forgegit/model/git_repo.py
+++ b/ForgeGit/forgegit/model/git_repo.py
@@ -21,6 +21,7 @@
 import logging
 import tempfile
 from datetime import datetime
+from contextlib import contextmanager
 
 import tg
 import git
@@ -631,6 +632,14 @@
             'total': total,
         }
 
+    @contextmanager
+    def _shared_clone(self, from_path):
+        tmp_path = tempfile.mkdtemp()
+        self._git.git.clone('--bare', '--shared', from_path, tmp_path)
+        tmp_repo = GitImplementation(Object(full_fs_path=tmp_path))
+        yield tmp_repo
+        shutil.rmtree(tmp_path, ignore_errors=True)
+
     def merge_base(self, mr):
         g = self._git.git
         g.fetch(mr.app.repo.full_fs_path, mr.target_branch)
@@ -642,11 +651,20 @@
 
         Must be called within mr.push_downstream_context()
         """
-        base = self.merge_base(mr)
-        return list(c.app.repo.log(
-            mr.downstream.commit_id,
-            exclude=base,
-            id_only=False))
+        use_tmp_dir = tg.config.get('scm.merge_list.git.use_tmp_dir', False)
+        use_tmp_dir = asbool(use_tmp_dir)
+        if not use_tmp_dir:
+            base = self.merge_base(mr)
+            return list(c.app.repo.log(
+                mr.downstream.commit_id,
+                exclude=base,
+                id_only=False))
+        with self._shared_clone(self._repo.full_fs_path) as tmp_repo:
+            base = tmp_repo.merge_base(mr)
+            return list(tmp_repo.log(
+                [mr.downstream.commit_id],
+                exclude=[base],
+                id_only=False))
 
 
 class _OpenedGitBlob(object):
diff --git a/ForgeGit/forgegit/tests/model/test_repository.py b/ForgeGit/forgegit/tests/model/test_repository.py
index e516f35..936fbee 100644
--- a/ForgeGit/forgegit/tests/model/test_repository.py
+++ b/ForgeGit/forgegit/tests/model/test_repository.py
@@ -143,6 +143,14 @@
         ThreadLocalORMSession.flush_all()
         ThreadLocalORMSession.close_all()
 
+    @property
+    def merge_request(self):
+        cid = '5c47243c8e424136fd5cdd18cd94d34c66d1955c'
+        return M.MergeRequest(
+            downstream={'commit_id': cid},
+            source_branch='zz',
+            target_branch='master')
+
     def test_init(self):
         repo = GM.Repository(
             name='testgit.git',
@@ -686,25 +694,11 @@
         assert_equals(diffs, expected)
 
     def test_merge_base(self):
-        mr = M.MergeRequest(
-            downstream={
-                'commit_id': '5c47243c8e424136fd5cdd18cd94d34c66d1955c',
-            },
-            source_branch='zz',
-            target_branch='master',
-        )
-        res = self.repo._impl.merge_base(mr)
+        res = self.repo._impl.merge_base(self.merge_request)
         assert_equal(res, '1e146e67985dcd71c74de79613719bef7bddca4a')
 
     def test_merge_request_commits(self):
-        mr = M.MergeRequest(
-            downstream={
-                'commit_id': '5c47243c8e424136fd5cdd18cd94d34c66d1955c',
-            },
-            source_branch='zz',
-            target_branch='master',
-        )
-        res = self.repo.merge_request_commits(mr)
+        res = self.repo.merge_request_commits(self.merge_request)
         expected = [
             {'authored': {
                 'date': datetime.datetime(2013, 3, 28, 18, 54, 16),
@@ -722,6 +716,18 @@
              'size': None}]
         assert_equals(res, expected)
 
+    def test_merge_request_commits_tmp_dir(self):
+        """
+        repo.merge_request_commits should return the same result with and
+        without scm.merge_list.git.use_tmp_dir option enabled
+        """
+        mr = self.merge_request
+        res_without_tmp = self.repo.merge_request_commits(mr)
+        opt = {'scm.merge_list.git.use_tmp_dir': True}
+        with h.push_config(tg.config, **opt):
+            res_with_tmp = self.repo.merge_request_commits(mr)
+        assert_equals(res_without_tmp, res_with_tmp)
+
 
 class TestGitImplementation(unittest.TestCase):