← Back to team overview

launchpad-reviewers team mailing list archive

Re: [Merge] lp:~blr/turnip/api-init-with-alternates into lp:turnip

 

Review: Needs Fixing code



Diff comments:

> === modified file 'turnip/api/store.py'
> --- turnip/api/store.py	2015-04-25 17:46:51 +0000
> +++ turnip/api/store.py	2015-04-28 10:11:43 +0000
> @@ -1,8 +1,10 @@
>  # Copyright 2015 Canonical Ltd.  All rights reserved.
>  
> +from contextlib import contextmanager
>  import itertools
>  import os
>  import shutil
> +import uuid
>  
>  from pygit2 import (
>      clone_repository,
> @@ -63,6 +65,10 @@
>          }
>  
>  
> +def is_bare_repo(repo_path):
> +    return not os.path.exists(os.path.join(repo_path, '.git'))
> +
> +
>  def is_valid_new_path(path):
>      """Verify repo path is new, or raise Exception."""
>      if os.path.exists(path):
> @@ -70,7 +76,25 @@
>      return True
>  
>  
> -def init_repo(repo_path, clone_from=None, clone_refs=False, is_bare=True):
> +def alternates_path(repo_path):
> +    """Git object alternates path.
> +    See http://git-scm.com/docs/gitrepository-layout
> +    """
> +    return os.path.join(repo_path, 'objects', 'info', 'alternates')
> +
> +
> +def write_alternates(repo_path, alternate_repo_paths):
> +    with open(alternates_path(repo_path), "w") as f:
> +        for path in alternate_repo_paths:
> +            if is_bare_repo(path):
> +                objects_path = os.path.join(path, 'objects')
> +            else:
> +                objects_path = os.path.join(path, '.git', 'objects')
> +            f.write("{}\n".format(objects_path))
> +
> +
> +def init_repo(repo_path, clone_from=None, clone_refs=False,
> +              alternate_repo_paths=None, is_bare=True):
>      """Initialise a new git repository or clone from existing."""
>      assert is_valid_new_path(repo_path)
>      init_repository(repo_path, is_bare)
> @@ -97,12 +121,41 @@
>              for ref in from_repo.listall_references():
>                  to_repo.create_reference(
>                      ref, from_repo.lookup_reference(ref).target)
> +
> +    if alternate_repo_paths:
> +        write_alternates(repo_path, alternate_repo_paths)
>      return repo_path
>  
>  
> +@contextmanager
>  def open_repo(repo_path):
> -    """Open an existing git repository."""
> -    return Repository(repo_path)
> +    """Open an existing git repository. Optionally create an
> +    ephemeral repository with alternates if repo_path contains ':'.
> +    """
> +    (repo_store, name) = os.path.split(repo_path)

This seems prone to failure. Can you pass the store root and repo name as separate arguments?

> +    if ':' in name:
> +        try:
> +            # create ephemeral repo with alternates set from both
> +            repos = [os.path.join(repo_store, repo)
> +                     for repo in name.split(':')]
> +            tmp_repo_path = os.path.join(repo_store,
> +                                         'ephemeral-' + uuid.uuid4().hex)
> +            ephemeral_repo_path = init_repo(
> +                tmp_repo_path,
> +                alternate_repo_paths=repos)
> +            repo = Repository(ephemeral_repo_path)
> +            repo.ephemeral = True
> +            yield repo
> +        finally:
> +            cleanup_repo(repo)
> +    else:
> +        yield Repository(repo_path)
> +
> +
> +def cleanup_repo(repo):
> +    """Remove ephemeral repo."""
> +    if hasattr(repo, 'ephemeral'):
> +        delete_repo(repo.path)

This function and repo.ephemeral can go away now that it's only called in the ephemeral case. You can just use delete_repo directly.

>  
>  
>  def delete_repo(repo_path):
> @@ -112,27 +165,27 @@
>  
>  def get_refs(repo_path):
>      """Return all refs for a git repository."""
> -    repo = open_repo(repo_path)
> -    refs = {}
> -    for ref in repo.listall_references():
> -        git_object = repo.lookup_reference(ref).peel()
> -        # Filter non-unicode refs, as refs are treated as unicode
> -        # given json is unable to represent arbitrary byte strings.
> -        try:
> -            ref.decode('utf-8')
> -        except UnicodeDecodeError:
> -            pass
> -        else:
> -            refs.update(format_ref(ref, git_object))
> -    return refs
> +    with open_repo(repo_path) as repo:
> +        refs = {}
> +        for ref in repo.listall_references():
> +            git_object = repo.lookup_reference(ref).peel()
> +            # Filter non-unicode refs, as refs are treated as unicode
> +            # given json is unable to represent arbitrary byte strings.
> +            try:
> +                ref.decode('utf-8')
> +            except UnicodeDecodeError:
> +                pass
> +            else:
> +                refs.update(format_ref(ref, git_object))
> +        return refs
>  
>  
>  def get_ref(repo_path, ref):
>      """Return a specific ref for a git repository."""
> -    repo = open_repo(repo_path)
> -    git_object = repo.lookup_reference(ref.encode('utf-8')).peel()
> -    ref_obj = format_ref(ref, git_object)
> -    return ref_obj
> +    with open_repo(repo_path) as repo:
> +        git_object = repo.lookup_reference(ref.encode('utf-8')).peel()
> +        ref_obj = format_ref(ref, git_object)
> +        return ref_obj
>  
>  
>  def get_common_ancestor_diff(repo_path, sha1_target, sha1_source,
> @@ -143,9 +196,9 @@
>      :param sha1_source: source sha1 for merge base.
>      :param context_lines: num unchanged lines that define a hunk boundary.
>      """
> -    repo = open_repo(repo_path)
> -    common_ancestor = repo.merge_base(sha1_target, sha1_source)
> -    return get_diff(repo_path, common_ancestor, sha1_source, context_lines)
> +    with open_repo(repo_path) as repo:
> +        common_ancestor = repo.merge_base(sha1_target, sha1_source)
> +        return get_diff(repo_path, common_ancestor, sha1_source, context_lines)
>  
>  
>  def get_merge_diff(repo_path, sha1_base, sha1_head, context_lines=3):
> @@ -155,23 +208,25 @@
>      :param sha1_head: source sha1 for merge.
>      :param context_lines: num unchanged lines that define a hunk boundary.
>      """
> -    repo = open_repo(repo_path)
> -    merged_index = repo.merge_commits(sha1_base, sha1_head)
> -    conflicts = set()
> -    if merged_index.conflicts is not None:
> -        for conflict in list(merged_index.conflicts):
> -            path = [entry for entry in conflict if entry is not None][0].path
> -            conflicts.add(path)
> -            merged_file = repo.merge_file_from_index(*conflict)
> -            blob_oid = repo.create_blob(merged_file)
> -            merged_index.add(IndexEntry(path, blob_oid, GIT_FILEMODE_BLOB))
> -            del merged_index.conflicts[path]
> -    diff = merged_index.diff_to_tree(
> -        repo[sha1_base].tree, context_lines=context_lines).patch
> -    shas = [sha1_base, sha1_head]
> -    commits = [get_commit(repo_path, sha, repo) for sha in shas]
> -    diff = {'commits': commits, 'patch': diff, 'conflicts': sorted(conflicts)}
> -    return diff
> +    with open_repo(repo_path) as repo:
> +        merged_index = repo.merge_commits(sha1_base, sha1_head)
> +        conflicts = set()
> +        if merged_index.conflicts is not None:
> +            for conflict in list(merged_index.conflicts):
> +                path = [entry for entry in conflict
> +                        if entry is not None][0].path
> +                conflicts.add(path)
> +                merged_file = repo.merge_file_from_index(*conflict)
> +                blob_oid = repo.create_blob(merged_file)
> +                merged_index.add(IndexEntry(path, blob_oid, GIT_FILEMODE_BLOB))
> +                del merged_index.conflicts[path]
> +        diff = merged_index.diff_to_tree(
> +            repo[sha1_base].tree, context_lines=context_lines).patch
> +        shas = [sha1_base, sha1_head]
> +        commits = [get_commit(repo_path, sha, repo) for sha in shas]
> +        diff = {'commits': commits, 'patch': diff,
> +                'conflicts': sorted(conflicts)}
> +        return diff
>  
>  
>  def get_diff(repo_path, sha1_from, sha1_to, context_lines=3):
> @@ -181,15 +236,15 @@
>      :param sha1_to: diff to sha1.
>      :param context_lines: num unchanged lines that define a hunk boundary.
>      """
> -    repo = open_repo(repo_path)
> -    shas = [sha1_from, sha1_to]
> -    commits = [get_commit(repo_path, sha, repo) for sha in shas]
> -    diff = {
> -        'commits': commits,
> -        'patch': repo.diff(commits[0]['sha1'], commits[1]['sha1'],
> -                           False, 0, context_lines).patch
> +    with open_repo(repo_path) as repo:
> +        shas = [sha1_from, sha1_to]
> +        commits = [get_commit(repo_path, sha, repo) for sha in shas]
> +        diff = {
> +            'commits': commits,
> +            'patch': repo.diff(commits[0]['sha1'], commits[1]['sha1'],
> +                               False, 0, context_lines).patch
>          }
> -    return diff
> +        return diff
>  
>  
>  def get_log(repo_path, start=None, limit=None, stop=None):
> @@ -199,37 +254,34 @@
>      :param limit: limit number of commits to return.
>      :param stop: ignore a commit (and its ancestors).
>      """
> -    repo = open_repo(repo_path)
> -    if not start:
> -        start = repo.head.target  # walk from HEAD
> -    walker = repo.walk(start)
> -    if stop:
> -        walker.hide(stop)  # filter stop sha1 and its ancestors
> -    if limit > 0:
> -        walker = itertools.islice(walker, limit)
> -    commits = [format_commit(commit) for commit in walker]
> -    return commits
> +    with open_repo(repo_path) as repo:
> +        if not start:
> +            start = repo.head.target  # walk from HEAD
> +        walker = repo.walk(start)
> +        if stop:
> +            walker.hide(stop)  # filter stop sha1 and its ancestors
> +        if limit > 0:
> +            walker = itertools.islice(walker, limit)
> +        return [format_commit(commit) for commit in walker]
>  
>  
>  def get_commit(repo_path, commit_oid, repo=None):
>      """Return a single commit object from an oid."""
> -    if not repo:
> -        repo = open_repo(repo_path)
> -    git_object = repo.get(commit_oid)
> -    if git_object is None:
> -        raise GitError('Object {} does not exist in repository {}.'.format(
> -            commit_oid, repo_path))
> -    commit = format_commit(git_object)
> -    return commit
> +    with open_repo(repo_path) as repo:

This now ignores the repo argument. If nothing uses it, it can be removed from the declaration.

> +        git_object = repo.get(commit_oid)
> +        if git_object is None:
> +            raise GitError('Object {} does not exist in repository {}.'.format(
> +                commit_oid, repo_path))
> +        return format_commit(git_object)
>  
>  
>  def get_commits(repo_path, commit_oids):
>      """Return a collection of commit objects from a list of oids."""
> -    repo = open_repo(repo_path)
> -    commits = []
> -    for commit in commit_oids:
> -        try:
> -            commits.append(get_commit(repo_path, commit, repo))
> -        except GitError:
> -            pass
> -    return commits
> +    with open_repo(repo_path) as repo:
> +        commits = []
> +        for commit in commit_oids:
> +            try:
> +                commits.append(get_commit(repo_path, commit, repo))
> +            except GitError:
> +                pass
> +        return commits
> 
> === modified file 'turnip/api/tests/test_api.py'
> --- turnip/api/tests/test_api.py	2015-04-27 13:16:08 +0000
> +++ turnip/api/tests/test_api.py	2015-04-28 10:11:43 +0000
> @@ -74,6 +74,59 @@
>          self.assertEqual(200, resp.status_code)
>          self.assertIn(new_repo_path, resp.json['repo_url'])
>  
> +    def test_cross_repo_merge_diff(self):
> +        """Merge diff can be requested across 2 repositories."""
> +        factory = RepoFactory(self.repo_store)
> +        c1 = factory.add_commit('foo', 'foobar.txt')
> +        factory.set_head(c1)
> +
> +        repo2_name = uuid.uuid4().hex
> +        factory2 = RepoFactory(
> +            os.path.join(self.repo_root, repo2_name), clone_from=factory)
> +        c2 = factory.add_commit('bar', 'foobar.txt', parents=[c1])
> +        c3 = factory2.add_commit('baz', 'foobar.txt', parents=[c1])
> +
> +        resp = self.app.get('/repo/{}:{}/compare-merge/{}:{}'.format(
> +            self.repo_path, repo2_name, c2, c3))
> +        self.assertIn('-bar', resp.json['patch'])
> +
> +    def test_cross_repo_diff(self):
> +        """Diff can be requested across 2 repositories."""
> +        factory = RepoFactory(self.repo_store)
> +        c1 = factory.add_commit('foo', 'foobar.txt')
> +        factory.set_head(c1)
> +
> +        repo2_name = uuid.uuid4().hex
> +        factory2 = RepoFactory(
> +            os.path.join(self.repo_root, repo2_name), clone_from=factory)
> +        c2 = factory.add_commit('bar', 'foobar.txt', parents=[c1])
> +        c3 = factory2.add_commit('baz', 'foobar.txt', parents=[c1])
> +
> +        resp = self.app.get('/repo/{}:{}/compare/{}..{}'.format(
> +            self.repo_path, repo2_name, c2, c3))
> +        self.assertIn('-bar', resp.json['patch'])
> +        self.assertIn('+baz', resp.json['patch'])
> +
> +    def test_cross_repo_diff_invalid_repo(self):
> +        """Cross repo diff with invalid repo returns HTTP 404."""
> +        resp = self.app.get('/repo/1:2/compare-merge/3:4', expect_errors=True)
> +        self.assertEqual(404, resp.status_code)
> +
> +    def test_cross_repo_diff_invalid_commit(self):
> +        """Cross repo diff with an invalid commit returns HTTP 404."""
> +        factory = RepoFactory(self.repo_store)
> +        c1 = factory.add_commit('foo', 'foobar.txt')
> +        factory.set_head(c1)
> +
> +        repo2_name = uuid.uuid4().hex
> +        RepoFactory(
> +            os.path.join(self.repo_root, repo2_name), clone_from=factory)
> +        c2 = factory.add_commit('bar', 'foobar.txt', parents=[c1])
> +
> +        resp = self.app.get('/repo/{}:{}/diff/{}:{}'.format(
> +            self.repo_path, repo2_name, c2, 'invalid'), expect_errors=True)
> +        self.assertEqual(404, resp.status_code)
> +
>      def test_repo_delete(self):
>          self.app.post_json('/repo', {'repo_path': self.repo_path})
>          resp = self.app.delete('/repo/{}'.format(self.repo_path))
> @@ -244,12 +297,15 @@
>          """Ensure expected changes exist in diff patch."""
>          repo = RepoFactory(self.repo_store)
>          c1 = repo.add_commit('foo\nbar\nbaz\n', 'blah.txt')
> -        c2_right = repo.add_commit('quux\nbar\nbaz\n', 'blah.txt', parents=[c1])
> -        c3_right = repo.add_commit('quux\nbar\nbaz\n', 'blah.txt', parents=[c2_right])
> +        c2_right = repo.add_commit('quux\nbar\nbaz\n', 'blah.txt',
> +                                   parents=[c1])
> +        c3_right = repo.add_commit('quux\nbar\nbaz\n', 'blah.txt',
> +                                   parents=[c2_right])
>          c2_left = repo.add_commit('foo\nbar\nbar\n', 'blah.txt', parents=[c1])
> -        c3_left = repo.add_commit('foo\nbar\nbar\n', 'blah.txt', parents=[c2_left])
> +        c3_left = repo.add_commit('foo\nbar\nbar\n', 'blah.txt',
> +                                  parents=[c2_left])
>  
> -        resp = self.app.get('/repo/{}/compare-merge/{}/{}'.format(
> +        resp = self.app.get('/repo/{}/compare-merge/{}:{}'.format(
>              self.repo_path, c3_right, c3_left))
>          self.assertIn(' quux', resp.json_body['patch'])
>          self.assertIn('-baz', resp.json_body['patch'])
> @@ -264,7 +320,7 @@
>          c2_left = repo.add_commit('foo\nbar\n', 'blah.txt', parents=[c1])
>          c2_right = repo.add_commit('foo\nbaz\n', 'blah.txt', parents=[c1])
>  
> -        resp = self.app.get('/repo/{}/compare-merge/{}/{}'.format(
> +        resp = self.app.get('/repo/{}/compare-merge/{}:{}'.format(
>              self.repo_path, c2_left, c2_right))
>          self.assertIn(dedent("""\
>              +<<<<<<< blah.txt
> 
> === modified file 'turnip/api/tests/test_helpers.py'
> --- turnip/api/tests/test_helpers.py	2015-04-14 15:12:31 +0000
> +++ turnip/api/tests/test_helpers.py	2015-04-28 10:11:43 +0000
> @@ -2,9 +2,12 @@
>  
>  import itertools
>  import os
> +import urllib
> +import urlparse
>  import uuid
>  
>  from pygit2 import (
> +    clone_repository,
>      init_repository,
>      GIT_FILEMODE_BLOB,
>      GIT_OBJ_COMMIT,
> @@ -29,7 +32,7 @@
>      """Builds a git repository in a user defined state."""
>  
>      def __init__(self, repo_path=None, num_commits=None,
> -                 num_branches=None, num_tags=None):
> +                 num_branches=None, num_tags=None, clone_from=None):
>          self.author = Signature('Test Author', 'author@xxxxxxx')
>          self.branches = []
>          self.committer = Signature('Test Commiter', 'committer@xxxxxxx')
> @@ -38,7 +41,10 @@
>          self.num_commits = num_commits
>          self.num_tags = num_tags
>          self.repo_path = repo_path
> -        self.repo = self.init_repo()
> +        if clone_from:
> +            self.repo = self.clone_repo(clone_from)
> +        else:
> +            self.repo = self.init_repo()
>  
>      @property
>      def commits(self):
> @@ -62,6 +68,9 @@
>                                   blob_content, tree_id, parents)
>          return oid
>  
> +    def set_head(self, oid):
> +        self.repo.create_reference('refs/heads/master', oid)
> +
>      def add_branch(self, name, oid):
>          commit = self.repo.get(oid)
>          branch = self.repo.create_branch('branch-{}'.format(name), commit)
> @@ -133,6 +142,12 @@
>      def init_repo(self):
>          return init_repository(self.repo_path)
>  
> +    def clone_repo(self, repo_factory):
> +        """Return a pygit2 repo object cloned from an existing factory repo."""
> +        clone_from_url = urlparse.urljoin(
> +            'file:', urllib.pathname2url(repo_factory.repo.path))
> +        return clone_repository(clone_from_url, self.repo_path)
> +
>      def build(self):
>          """Return a repo, optionally with generated commits and tags."""
>          if self.num_branches:
> 
> === modified file 'turnip/api/tests/test_store.py'
> --- turnip/api/tests/test_store.py	2015-04-25 12:50:07 +0000
> +++ turnip/api/tests/test_store.py	2015-04-28 10:11:43 +0000
> @@ -8,20 +8,29 @@
>  
>  import os.path
>  import subprocess
> +import uuid
>  
> -from fixtures import TempDir
> +from fixtures import (
> +    EnvironmentVariable,
> +    TempDir,
> +    )
>  import pygit2
>  from testtools import TestCase
>  
>  from turnip.api import store
> -from turnip.api.tests.test_helpers import RepoFactory
> +from turnip.api.tests.test_helpers import (
> +    open_repo,
> +    RepoFactory,
> +    )
>  
>  
>  class InitTestCase(TestCase):
>  
>      def setUp(self):
>          super(InitTestCase, self).setUp()
> -        self.root = self.useFixture(TempDir()).path
> +        self.repo_store = self.useFixture(TempDir()).path
> +        self.useFixture(EnvironmentVariable("REPO_STORE", self.repo_store))
> +        self.repo_path = os.path.join(self.repo_store, uuid.uuid1().hex)
>  
>      def assertAllLinkCounts(self, link_count, path):
>          count = 0
> @@ -44,19 +53,57 @@
>              self.assertNotIn(absent, out)
>  
>      def makeOrig(self):
> -        self.orig_path = os.path.join(self.root, 'orig/')
> +        self.orig_path = os.path.join(self.repo_store, 'orig/')
>          orig = RepoFactory(
>              self.orig_path, num_branches=3, num_commits=2).build()
>          self.orig_refs = orig.listall_references()
>          self.master_oid = orig.lookup_reference('refs/heads/master').target
>          self.orig_objs = os.path.join(self.orig_path, '.git/objects')
>  
> +    def assert_alternate_exists(self, alternate_path, repo_path):
> +        """Assert alternate_path exists in alternates at repo_path."""
> +        objects_path = '{}\n'.format(
> +            os.path.join(alternate_path, 'objects'))
> +        with open(store.alternates_path(repo_path), 'r') as alts:
> +            alts_content = alts.read()
> +            self.assertIn(objects_path, alts_content)
> +
>      def test_from_scratch(self):
> -        path = os.path.join(self.root, 'repo/')
> +        path = os.path.join(self.repo_store, 'repo/')
>          self.assertEqual(path, store.init_repo(path))
>          r = pygit2.Repository(path)
>          self.assertEqual([], r.listall_references())
>  
> +    def test_open_ephemeral_repo(self):
> +        """Opening a repo where a repo name contains ':' should return
> +        a new ephemeral repo.
> +        """
> +        repos = [uuid.uuid4().hex, uuid.uuid4().hex]
> +        repo_name = '{}:{}'.format(repos[0], repos[1])
> +        repo_path = os.path.join(self.repo_store, repo_name)
> +        alt_path = os.path.join(self.repo_store, repos[0])
> +        with store.open_repo(repo_path) as repo:
> +            self.assert_alternate_exists(alt_path, repo.path)
> +            self.assertTrue(repo.ephemeral)
> +
> +    def test_repo_with_alternates(self):
> +        """Ensure objects path is defined correctly in repo alternates."""
> +        factory = RepoFactory(self.repo_path)
> +        new_repo_path = os.path.join(self.repo_store, uuid.uuid1().hex)
> +        repo_path_with_alt = store.init_repo(
> +            new_repo_path, alternate_repo_paths=[factory.repo.path])
> +        self.assert_alternate_exists(factory.repo.path, repo_path_with_alt)
> +
> +    def test_repo_alternates_objects_shared(self):
> +        """Ensure objects are shared from alternate repo."""
> +        factory = RepoFactory(self.repo_path)
> +        commit_oid = factory.add_commit('foo', 'foobar.txt')
> +        new_repo_path = os.path.join(self.repo_store, uuid.uuid4().hex)
> +        repo_path_with_alt = store.init_repo(
> +            new_repo_path, alternate_repo_paths=[factory.repo.path])
> +        repo_with_alt = open_repo(repo_path_with_alt)
> +        self.assertEqual(commit_oid.hex, repo_with_alt.get(commit_oid).hex)
> +
>      def test_clone_with_refs(self):
>          self.makeOrig()
>          self.assertAllLinkCounts(1, self.orig_objs)
> @@ -64,7 +111,7 @@
>          # init_repo with clone_from=orig and clone_refs=True creates a
>          # repo with the same set of refs. And the objects are copied
>          # too.
> -        to_path = os.path.join(self.root, 'to/')
> +        to_path = os.path.join(self.repo_store, 'to/')
>          store.init_repo(to_path, clone_from=self.orig_path, clone_refs=True)
>          to = pygit2.Repository(to_path)
>          self.assertIsNot(None, to[self.master_oid])
> @@ -88,7 +135,7 @@
>  
>          # init_repo with clone_from=orig and clone_refs=False creates a
>          # repo without any refs, but the objects are copied.
> -        to_path = os.path.join(self.root, 'to/')
> +        to_path = os.path.join(self.repo_store, 'to/')
>          store.init_repo(to_path, clone_from=self.orig_path, clone_refs=False)
>          to = pygit2.Repository(to_path)
>          self.assertIsNot(None, to[self.master_oid])
> 
> === modified file 'turnip/api/views.py'
> --- turnip/api/views.py	2015-04-27 13:16:08 +0000
> +++ turnip/api/views.py	2015-04-28 10:11:43 +0000
> @@ -121,6 +121,7 @@
>      Two dots provides a simple diff, equivalent to `git diff A B`.
>      Three dots provides the symmetric or common ancestor diff, equivalent
>      to `git diff $(git-merge-base A B) B`.
> +    {name} can be two : separated repositories, for a cross repository diff.
>      """
>      def __init__(self, request):
>          super(DiffAPI, self).__init__()
> @@ -146,11 +147,12 @@
>          return patch
>  
>  
> -@resource(path='/repo/{name}/compare-merge/{base}/{head}')
> +@resource(path='/repo/{name}/compare-merge/{base}:{head}')
>  class DiffMergeAPI(BaseAPI):
>      """Provides an HTTP API for merge previews.
>  
>      {head} will be merged into {base} and the diff from {base} returned.
> +    {name} can be two : separated repositories, for a cross repository diff.
>      """
>      def __init__(self, request):
>          super(DiffMergeAPI, self).__init__()
> 


-- 
https://code.launchpad.net/~blr/turnip/api-init-with-alternates/+merge/256715
Your team Launchpad code reviewers is subscribed to branch lp:turnip.


References