← Back to team overview

launchpad-reviewers team mailing list archive

Re: [Merge] lp:~cjwatson/launchpad/git-mp-diffs into lp:launchpad

 

Review: Approve code



Diff comments:

> === modified file 'database/schema/security.cfg'
> --- database/schema/security.cfg	2015-04-21 10:34:24 +0000
> +++ database/schema/security.cfg	2015-04-29 14:40:47 +0000
> @@ -1,4 +1,4 @@
> -# Copyright 2009-2013 Canonical Ltd.  This software is licensed under the
> +# Copyright 2009-2015 Canonical Ltd.  This software is licensed under the
>  # GNU Affero General Public License version 3 (see the file LICENSE).
>  #
>  # Possible permissions: SELECT, INSERT, UPDATE, EXECUTE
> @@ -1937,6 +1937,8 @@
>  public.distribution                     = SELECT
>  public.distroseries                     = SELECT
>  public.emailaddress                     = SELECT
> +public.gitref                           = SELECT
> +public.gitrepository                    = SELECT
>  public.incrementaldiff                  = SELECT, INSERT
>  public.job                              = SELECT, INSERT, UPDATE
>  public.karma                            = SELECT, INSERT
> 
> === modified file 'lib/lp/app/browser/stringformatter.py'
> --- lib/lp/app/browser/stringformatter.py	2013-12-12 05:06:11 +0000
> +++ lib/lp/app/browser/stringformatter.py	2015-04-29 14:40:47 +0000
> @@ -1,4 +1,4 @@
> -# Copyright 2010-2012 Canonical Ltd.  This software is licensed under the
> +# Copyright 2010-2015 Canonical Ltd.  This software is licensed under the
>  # GNU Affero General Public License version 3 (see the file LICENSE).
>  
>  """TALES formatter for strings."""
> @@ -884,7 +884,9 @@
>          for row, line in enumerate(text.splitlines()[:max_format_lines]):
>              result.append('<tr id="diff-line-%s">' % (row + 1))
>              result.append('<td class="line-no">%s</td>' % (row + 1))
> -            if line.startswith('==='):
> +            if (line.startswith('===') or
> +                    line.startswith('diff') or
> +                    line.startswith('index')):
>                  css_class = 'diff-file text'
>                  header_next = True
>              elif (header_next and
> 
> === modified file 'lib/lp/app/browser/tests/test_stringformatter.py'
> --- lib/lp/app/browser/tests/test_stringformatter.py	2013-12-13 02:09:14 +0000
> +++ lib/lp/app/browser/tests/test_stringformatter.py	2015-04-29 14:40:47 +0000
> @@ -1,4 +1,4 @@
> -# Copyright 2010-2011 Canonical Ltd.  This software is licensed under the
> +# Copyright 2010-2015 Canonical Ltd.  This software is licensed under the
>  # GNU Affero General Public License version 3 (see the file LICENSE).
>  
>  """Unit tests for the string TALES formatter."""
> @@ -165,7 +165,7 @@
>  
>  
>  class TestLinkifyingProtocols(TestCaseWithFactory):
> -    
> +
>      layer = DatabaseFunctionalLayer
>  
>      def test_normal_set(self):
> @@ -357,6 +357,40 @@
>               'diff-comment text'],
>              [str(tag['class']) for tag in text])
>  
> +    def test_cssClasses_git(self):
> +        # Git diffs look slightly different, so check that they also end up
> +        # with the correct CSS classes.
> +        diff = dedent('''\
> +            diff --git a/tales.py b/tales.py
> +            index aaaaaaa..bbbbbbb 100644
> +            --- a/tales.py
> +            +++ b/tales.py
> +            @@ -2435,6 +2435,8 @@
> +                 def format_diff(self):
> +            -        removed this line
> +            +        added this line
> +            -------- a sql style comment
> +            ++++++++ a line of pluses
> +            ''')
> +        html = FormattersAPI(diff).format_diff()
> +        line_numbers = find_tags_by_class(html, 'line-no')
> +        self.assertEqual(
> +            ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'],
> +            [tag.renderContents() for tag in line_numbers])
> +        text = find_tags_by_class(html, 'text')
> +        self.assertEqual(
> +            ['diff-file text',
> +             'diff-file text',
> +             'diff-header text',
> +             'diff-header text',
> +             'diff-chunk text',
> +             'text',
> +             'diff-removed text',
> +             'diff-added text',
> +             'diff-removed text',
> +             'diff-added text'],
> +            [str(tag['class']) for tag in text])
> +
>      def test_config_value_limits_line_count(self):
>          # The config.diff.max_line_format contains the maximum number of lines
>          # to format.
> 
> === modified file 'lib/lp/code/githosting.py'
> --- lib/lp/code/githosting.py	2015-03-31 00:59:44 +0000
> +++ lib/lp/code/githosting.py	2015-04-29 14:40:47 +0000
> @@ -101,3 +101,35 @@
>          except ValueError as e:
>              raise GitRepositoryScanFault(
>                  "Failed to decode commit-scan response: %s" % unicode(e))
> +
> +    def getMergeDiff(self, path, base, head, logger=None):
> +        """Get the merge preview diff between two commits.
> +
> +        :return: A dict mapping 'commits' to a list of commits between
> +            'base' and 'head' (formatted as with `getCommits`), 'patch' to
> +            the text of the diff between 'base' and 'head', and 'conflicts'
> +            to a list of conflicted paths.
> +        """
> +        try:
> +            if logger is not None:
> +                logger.info(
> +                    "Requesting merge diff for %s from %s to %s" % (
> +                        path, base, head))
> +            response = self._makeSession().get(
> +                urlutils.join(
> +                    self.endpoint, "repo", path, "compare-merge",
> +                    "%s:%s" % (base, head)),
> +                timeout=self.timeout)
> +        except Exception as e:
> +            raise GitRepositoryScanFault(
> +                "Failed to get merge diff from Git repository: %s" %
> +                unicode(e))
> +        if response.status_code != 200:
> +            raise GitRepositoryScanFault(
> +                "Failed to get merge diff from Git repository: %s" %
> +                unicode(e))
> +        try:
> +            return response.json()
> +        except ValueError as e:
> +            raise GitRepositoryScanFault(
> +                "Failed to decode merge-diff response: %s" % unicode(e))
> 
> === modified file 'lib/lp/code/model/branchmergeproposaljob.py'
> --- lib/lp/code/model/branchmergeproposaljob.py	2014-05-28 12:23:10 +0000
> +++ lib/lp/code/model/branchmergeproposaljob.py	2015-04-29 14:40:47 +0000
> @@ -1,4 +1,4 @@
> -# Copyright 2009-2013 Canonical Ltd.  This software is licensed under the
> +# Copyright 2009-2015 Canonical Ltd.  This software is licensed under the
>  # GNU Affero General Public License version 3 (see the file LICENSE).
>  
>  """Job classes related to BranchMergeProposals are in here.
> @@ -21,6 +21,7 @@
>      'UpdatePreviewDiffJob',
>      ]
>  
> +from contextlib import contextmanager
>  from datetime import (
>      datetime,
>      timedelta,
> @@ -39,7 +40,6 @@
>      Desc,
>      Or,
>      )
> -from storm.info import ClassAlias
>  from storm.locals import (
>      Int,
>      Reference,
> @@ -102,6 +102,7 @@
>  from lp.services.mail.sendmail import format_address_for_person
>  from lp.services.webapp import canonical_url
>  
> +
>  class BranchMergeProposalJobType(DBEnumeratedType):
>      """Values that ICodeImportJob.state can take."""
>  
> @@ -235,7 +236,7 @@
>          return '<%(job_type)s job for merge %(merge_id)s on %(branch)s>' % {
>              'job_type': self.context.job_type.name,
>              'merge_id': bmp.id,
> -            'branch': bmp.source_branch.unique_name,
> +            'branch': bmp.merge_source.unique_name,
>              }
>  
>      @classmethod
> @@ -278,12 +279,13 @@
>                  Job.id.is_in(Job.ready_jobs),
>                  BranchMergeProposalJob.branch_merge_proposal
>                      == BranchMergeProposal.id,
> -                BranchMergeProposal.source_branch == Branch.id,
> -                # A proposal isn't considered ready if it has no revisions,
> -                # or if it is hosted but pending a mirror.
> -                Branch.revision_count > 0,
> -                Or(Branch.next_mirror_time == None,
> -                   Branch.branch_type != BranchType.HOSTED)))
> +                Or(And(BranchMergeProposal.source_branch == Branch.id,
> +                       # A proposal isn't considered ready if it has no
> +                       # revisions, or if it is hosted but pending a mirror.
> +                       Branch.revision_count > 0,
> +                       Or(Branch.next_mirror_time == None,
> +                          Branch.branch_type != BranchType.HOSTED)),
> +                   BranchMergeProposal.source_git_repository != None)))
>          return (klass(job) for job in jobs)
>  
>      def getOopsVars(self):
> @@ -293,8 +295,8 @@
>          vars.extend([
>              ('branchmergeproposal_job_id', self.context.id),
>              ('branchmergeproposal_job_type', self.context.job_type.title),
> -            ('source_branch', bmp.source_branch.unique_name),
> -            ('target_branch', bmp.target_branch.unique_name)])
> +            ('source_branch', bmp.merge_source.unique_name),
> +            ('target_branch', bmp.merge_target.unique_name)])

Can you fix these two names? Nothing should depend on them.

>          return vars
>  
>  
> @@ -320,8 +322,8 @@
>  
>      def getOperationDescription(self):
>          return ('notifying people about the proposal to merge %s into %s' %
> -            (self.branch_merge_proposal.source_branch.bzr_identity,
> -             self.branch_merge_proposal.target_branch.bzr_identity))
> +            (self.branch_merge_proposal.merge_source.identity,
> +             self.branch_merge_proposal.merge_target.identity))
>  
>  
>  class UpdatePreviewDiffJob(BranchMergeProposalJobDerived):
> @@ -350,15 +352,23 @@
>          """Is this job ready to run?"""
>          bmp = self.branch_merge_proposal
>          url = canonical_url(bmp)
> -        if bmp.source_branch.last_scanned_id is None:
> -            raise UpdatePreviewDiffNotReady(
> -                'The source branch of %s has no revisions.' % url)
> -        if bmp.target_branch.last_scanned_id is None:
> -            raise UpdatePreviewDiffNotReady(
> -                'The target branch of %s has no revisions.' % url)
> -        if bmp.source_branch.pending_writes:
> -            raise BranchHasPendingWrites(
> -                'The source branch of %s has pending writes.' % url)
> +        if bmp.source_branch is not None:
> +            if bmp.source_branch.last_scanned_id is None:
> +                raise UpdatePreviewDiffNotReady(
> +                    'The source branch of %s has no revisions.' % url)
> +            if bmp.target_branch.last_scanned_id is None:
> +                raise UpdatePreviewDiffNotReady(
> +                    'The target branch of %s has no revisions.' % url)
> +            if bmp.source_branch.pending_writes:
> +                raise BranchHasPendingWrites(
> +                    'The source branch of %s has pending writes.' % url)
> +        else:
> +            if bmp.source_git_ref.commit_sha1 is None:
> +                raise UpdatePreviewDiffNotReady(
> +                    'The source branch of %s has not been scanned.' % url)
> +            if bmp.target_git_ref.commit_sha1 is None:
> +                raise UpdatePreviewDiffNotReady(
> +                    'The source branch of %s has not been scanned.' % url)
>  
>      def acquireLease(self, duration=600):
>          return self.job.acquireLease(duration)
> @@ -366,7 +376,13 @@
>      def run(self):
>          """See `IRunnableJob`."""
>          self.checkReady()
> -        with server(get_ro_server(), no_replace=True):
> +        if self.branch_merge_proposal.source_branch is not None:
> +            server_context = server(get_ro_server(), no_replace=True)
> +        else:
> +            # A no-op context manager.  (This could be simplified with
> +            # contextlib.ExitStack from Python 3.3.)
> +            server_context = contextmanager(lambda: (None for _ in [None]))()
> +        with server_context:
>              with BranchMergeProposalDelta.monitor(self.branch_merge_proposal):
>                  PreviewDiff.fromBranchMergeProposal(self.branch_merge_proposal)
>  
> @@ -655,22 +671,16 @@
>  
>      @staticmethod
>      def iterReady(job_type=None):
> -        from lp.code.model.branch import Branch
> -        SourceBranch = ClassAlias(Branch)
> -        TargetBranch = ClassAlias(Branch)
>          clauses = [
>              BranchMergeProposalJob.job == Job.id,
>              Job._status.is_in([JobStatus.WAITING, JobStatus.RUNNING]),
>              BranchMergeProposalJob.branch_merge_proposal ==
> -            BranchMergeProposal.id, BranchMergeProposal.source_branch ==
> -            SourceBranch.id, BranchMergeProposal.target_branch ==
> -            TargetBranch.id,
> +            BranchMergeProposal.id,
>              ]
>          if job_type is not None:
>              clauses.append(BranchMergeProposalJob.job_type == job_type)
> -        jobs = IMasterStore(Branch).find(
> -            (BranchMergeProposalJob, Job, BranchMergeProposal,
> -             SourceBranch, TargetBranch), And(*clauses))
> +        jobs = IMasterStore(BranchMergeProposalJob).find(
> +            (BranchMergeProposalJob, Job, BranchMergeProposal), And(*clauses))
>          # Order by the job status first (to get running before waiting), then

Do we know why it was like that? Possibly for performance, but that seems dubious.

>          # the date_created, then job type.  This should give us all creation
>          # jobs before comment jobs.
> @@ -680,7 +690,7 @@
>          # Now only return one job for any given merge proposal.
>          ready_jobs = []
>          seen_merge_proposals = set()
> -        for bmp_job, job, bmp, source, target in jobs:
> +        for bmp_job, job, bmp in jobs:
>              # If we've seen this merge proposal already, skip this job.
>              if bmp.id in seen_merge_proposals:
>                  continue
> 
> === modified file 'lib/lp/code/model/diff.py'
> --- lib/lp/code/model/diff.py	2014-02-26 13:38:39 +0000
> +++ lib/lp/code/model/diff.py	2015-04-29 14:40:47 +0000
> @@ -1,4 +1,4 @@
> -# Copyright 2009-2013 Canonical Ltd.  This software is licensed under the
> +# Copyright 2009-2015 Canonical Ltd.  This software is licensed under the
>  # GNU Affero General Public License version 3 (see the file LICENSE).
>  
>  """Implementation classes for IDiff, etc."""
> @@ -41,6 +41,7 @@
>  from zope.interface import implements
>  
>  from lp.app.errors import NotFoundError
> +from lp.code.githosting import GitHostingClient
>  from lp.code.interfaces.diff import (
>      IDiff,
>      IIncrementalDiff,
> @@ -233,7 +234,7 @@
>          return cls.fromFile(diff_content, size, filename)
>  
>      @classmethod
> -    def fromFile(cls, diff_content, size, filename=None):
> +    def fromFile(cls, diff_content, size, filename=None, strip_slashes=0):

strip_slashes is weird. strip_prefix_segments?

>          """Create a Diff from a textual diff.
>  
>          :diff_content: The diff text
> @@ -254,7 +255,8 @@
>              diff_content_bytes = diff_content.read(size)
>              diff_lines_count = len(diff_content_bytes.strip().split('\n'))
>          try:
> -            diffstat = cls.generateDiffstat(diff_content_bytes)
> +            diffstat = cls.generateDiffstat(
> +                diff_content_bytes, strip_slashes=strip_slashes)
>          except Exception:
>              getUtility(IErrorReportingUtility).raising(sys.exc_info())
>              # Set the diffstat to be empty.
> @@ -272,10 +274,13 @@
>                     removed_lines_count=removed_lines_count)
>  
>      @staticmethod
> -    def generateDiffstat(diff_bytes):
> +    def generateDiffstat(diff_bytes, strip_slashes=0):
>          """Generate statistics about the provided diff.
>  
>          :param diff_bytes: A unified diff, as bytes.
> +        :param strip_slashes: Strip the smallest prefix containing this many
> +            leading slashes from each file name found in the patch file, as
> +            with "patch -p".
>          :return: A map of {filename: (added_line_count, removed_line_count)}
>          """
>          file_stats = {}
> @@ -285,6 +290,8 @@
>              if not isinstance(patch, Patch):
>                  continue
>              path = patch.newname.split('\t')[0]
> +            if strip_slashes:
> +                path = path.split('/', strip_slashes)[-1]
>              file_stats[path] = tuple(patch.stats_values()[:2])
>          return file_stats
>  
> @@ -378,27 +385,39 @@
>          # diff was generated for absent branch revisions (e.g. the source
>          # or target branch was overwritten). Which means some entries for
>          # the same BMP may have much wider titles depending on the
> -        # branch history. It is particulary bad for rendering the diff
> +        # branch history. It is particularly bad for rendering the diff
>          # navigator 'select' widget in the UI.
> -        source_rev = bmp.source_branch.getBranchRevision(
> -            revision_id=self.source_revision_id)
> -        if source_rev and source_rev.sequence:
> -            source_revno = u'r{0}'.format(source_rev.sequence)
> -        else:
> -            source_revno = self.source_revision_id
> -        target_rev = bmp.target_branch.getBranchRevision(
> -            revision_id=self.target_revision_id)
> -        if target_rev and target_rev.sequence:
> -            target_revno = u'r{0}'.format(target_rev.sequence)
> -        else:
> -            target_revno = self.target_revision_id
> +        if bmp.source_branch is not None:
> +            source_revision = bmp.source_branch.getBranchRevision(
> +                revision_id=self.source_revision_id)
> +            if source_revision and source_revision.sequence:
> +                source_rev = u'r{0}'.format(source_revision.sequence)
> +            else:
> +                source_rev = self.source_revision_id
> +            target_revision = bmp.target_branch.getBranchRevision(
> +                revision_id=self.target_revision_id)
> +            if target_revision and target_revision.sequence:
> +                target_rev = u'r{0}'.format(target_revision.sequence)
> +            else:
> +                target_rev = self.target_revision_id
> +        else:
> +            # For Git, we shorten to seven characters since that's usual.
> +            # We should perhaps shorten only as far as preserves uniqueness,
> +            # but that requires talking to the hosting service and it's
> +            # unlikely to be a problem in practice.
> +            source_rev = self.source_revision_id[:7]
> +            target_rev = self.target_revision_id[:7]
>  
> -        return u'{0} into {1}'.format(source_revno, target_revno)
> +        return u'{0} into {1}'.format(source_rev, target_rev)
>  
>      @property
>      def has_conflicts(self):
>          return self.conflicts is not None and self.conflicts != ''
>  
> +    @staticmethod
> +    def getGitHostingClient():
> +        return GitHostingClient(config.codehosting.internal_git_api_endpoint)
> +
>      @classmethod
>      def fromBranchMergeProposal(cls, bmp):
>          """Create a `PreviewDiff` from a `BranchMergeProposal`.
> @@ -407,34 +426,60 @@
>          :param bmp: The `BranchMergeProposal` to generate a `PreviewDiff` for.
>          :return: A `PreviewDiff`.
>          """
> -        source_branch = bmp.source_branch.getBzrBranch()
> -        source_revision = source_branch.last_revision()
> -        target_branch = bmp.target_branch.getBzrBranch()
> -        target_revision = target_branch.last_revision()
> -        if bmp.prerequisite_branch is not None:
> -            prerequisite_branch = bmp.prerequisite_branch.getBzrBranch()
> +        if bmp.source_branch is not None:
> +            source_branch = bmp.source_branch.getBzrBranch()
> +            source_revision = source_branch.last_revision()
> +            target_branch = bmp.target_branch.getBzrBranch()
> +            target_revision = target_branch.last_revision()
> +            if bmp.prerequisite_branch is not None:
> +                prerequisite_branch = bmp.prerequisite_branch.getBzrBranch()
> +            else:
> +                prerequisite_branch = None
> +            diff, conflicts = Diff.mergePreviewFromBranches(
> +                source_branch, source_revision, target_branch,
> +                prerequisite_branch)
> +            preview = cls()
> +            preview.source_revision_id = source_revision.decode('utf-8')
> +            preview.target_revision_id = target_revision.decode('utf-8')
> +            preview.branch_merge_proposal = bmp
> +            preview.diff = diff
> +            preview.conflicts = u''.join(
> +                unicode(conflict) + '\n' for conflict in conflicts)
>          else:
> -            prerequisite_branch = None
> -        diff, conflicts = Diff.mergePreviewFromBranches(
> -            source_branch, source_revision, target_branch, prerequisite_branch)
> -        preview = cls()
> -        preview.source_revision_id = source_revision.decode('utf-8')
> -        preview.target_revision_id = target_revision.decode('utf-8')
> -        preview.branch_merge_proposal = bmp
> -        preview.diff = diff
> -        preview.conflicts = u''.join(
> -            unicode(conflict) + '\n' for conflict in conflicts)
> +            hosting_client = cls.getGitHostingClient()
> +            source_repository = bmp.source_git_repository
> +            target_repository = bmp.target_git_repository
> +            if source_repository == target_repository:
> +                path = source_repository.getInternalPath()
> +            else:
> +                path = "%s:%s" % (
> +                    target_repository.getInternalPath(),
> +                    source_repository.getInternalPath())
> +            response = hosting_client.getMergeDiff(
> +                path, bmp.source_git_ref.commit_sha1,
> +                bmp.target_git_ref.commit_sha1)

This could use an XXX about the lack of prereq support in the underlying API.

> +            source_revision_id = bmp.source_git_ref.commit_sha1
> +            target_revision_id = bmp.target_git_ref.commit_sha1
> +            if bmp.prerequisite_git_ref is not None:
> +                prerequisite_revision_id = bmp.prerequisite_git_ref.commit_sha1
> +            else:
> +                prerequisite_revision_id = None
> +            conflicts = u"".join(
> +                u"Conflict in %s\n" % path for path in response['conflicts'])
> +            preview = cls.create(
> +                bmp, response['patch'], source_revision_id, target_revision_id,
> +                prerequisite_revision_id, conflicts, strip_slashes=1)
>          del get_property_cache(bmp).preview_diffs
>          del get_property_cache(bmp).preview_diff
>          return preview
>  
>      @classmethod
>      def create(cls, bmp, diff_content, source_revision_id, target_revision_id,
> -               prerequisite_revision_id, conflicts):
> +               prerequisite_revision_id, conflicts, strip_slashes=0):
>          """Create a PreviewDiff with specified values.
>  
>          :param bmp: The `BranchMergeProposal` this diff references.
> -        :param diff_content: The text of the dift, as bytes.
> +        :param diff_content: The text of the diff, as bytes.
>          :param source_revision_id: The revision_id of the source branch.
>          :param target_revision_id: The revision_id of the target branch.
>          :param prerequisite_revision_id: The revision_id of the prerequisite
> @@ -444,7 +489,9 @@
>          """
>          filename = str(uuid1()) + '.txt'
>          size = len(diff_content)
> -        diff = Diff.fromFile(StringIO(diff_content), size, filename)
> +        diff = Diff.fromFile(
> +            StringIO(diff_content), size, filename,
> +            strip_slashes=strip_slashes)
>  
>          preview = cls()
>          preview.branch_merge_proposal = bmp
> 
> === modified file 'lib/lp/code/model/tests/test_branchmergeproposaljobs.py'
> --- lib/lp/code/model/tests/test_branchmergeproposaljobs.py	2014-05-29 07:08:17 +0000
> +++ lib/lp/code/model/tests/test_branchmergeproposaljobs.py	2015-04-29 14:40:47 +0000
> @@ -1,4 +1,4 @@
> -# Copyright 2010-2012 Canonical Ltd.  This software is licensed under the
> +# Copyright 2010-2015 Canonical Ltd.  This software is licensed under the
>  # GNU Affero General Public License version 3 (see the file LICENSE).
>  
>  """Tests for branch merge proposal jobs."""
> @@ -38,6 +38,7 @@
>      IUpdatePreviewDiffJob,
>      IUpdatePreviewDiffJobSource,
>      )
> +from lp.code.interfaces.gitrepository import GIT_FEATURE_FLAG
>  from lp.code.model.branchmergeproposaljob import (
>      BranchMergeProposalJob,
>      BranchMergeProposalJobDerived,
> @@ -219,6 +220,14 @@
>              JobRunner([job]).runAll()
>          self.checkExampleMerge(bmp.preview_diff.text)
>  
> +    def test_run_git(self):
> +        self.useFixture(FeatureFixture({GIT_FEATURE_FLAG: u"on"}))
> +        bmp, _, _, patch = self.createExampleGitMerge()
> +        job = UpdatePreviewDiffJob.create(bmp)
> +        with dbuser("merge-proposal-jobs"):
> +            JobRunner([job]).runAll()
> +        self.assertEqual(patch, bmp.preview_diff.text)
> +
>      def test_run_object_events(self):
>          # While the job runs a single IObjectModifiedEvent is issued when the
>          # preview diff has been calculated.
> @@ -514,6 +523,16 @@
>          self.assertEqual(job.branch_merge_proposal, bmp)
>          self.assertIsInstance(job, MergeProposalUpdatedEmailJob)
>  
> +    def test_iterReady_supports_git(self):
> +        # iterReady supports merge proposals based on Git.  (These are
> +        # currently considered ready regardless of scanning, since the hard
> +        # work is done by the backend.)

What if we don't have SHA-1s for the involved refs yet?

> +        self.useFixture(FeatureFixture({GIT_FEATURE_FLAG: u"on"}))
> +        bmp = self.factory.makeBranchMergeProposalForGit()
> +        [job] = self.job_source.iterReady()
> +        self.assertEqual(bmp, job.branch_merge_proposal)
> +        self.assertIsInstance(job, UpdatePreviewDiffJob)
> +
>  
>  class TestCodeReviewCommentEmailJob(TestCaseWithFactory):
>  
> 
> === modified file 'lib/lp/code/model/tests/test_diff.py'
> --- lib/lp/code/model/tests/test_diff.py	2014-02-26 13:38:39 +0000
> +++ lib/lp/code/model/tests/test_diff.py	2015-04-29 14:40:47 +0000
> @@ -1,4 +1,4 @@
> -# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
> +# Copyright 2009-2015 Canonical Ltd.  This software is licensed under the
>  # GNU Affero General Public License version 3 (see the file LICENSE).
>  
>  """Tests for Diff, etc."""
> @@ -8,6 +8,7 @@
>  from cStringIO import StringIO
>  from difflib import unified_diff
>  import logging
> +from textwrap import dedent
>  
>  from bzrlib import trace
>  from bzrlib.patches import (
> @@ -15,6 +16,7 @@
>      parse_patches,
>      RemoveLine,
>      )
> +from fixtures import MonkeyPatch
>  import transaction
>  from zope.security.proxy import removeSecurityProxy
>  
> @@ -24,11 +26,13 @@
>      IIncrementalDiff,
>      IPreviewDiff,
>      )
> +from lp.code.interfaces.gitrepository import GIT_FEATURE_FLAG
>  from lp.code.model.diff import (
>      Diff,
>      PreviewDiff,
>      )
>  from lp.code.model.directbranchcommit import DirectBranchCommit
> +from lp.services.features.testing import FeatureFixture
>  from lp.services.librarian.interfaces.client import (
>      LIBRARIAN_SERVER_DEFAULT_TIMEOUT,
>      )
> @@ -40,6 +44,7 @@
>      TestCaseWithFactory,
>      verifyObject,
>      )
> +from lp.testing.fakemethod import FakeMethod
>  from lp.testing.layers import (
>      LaunchpadFunctionalLayer,
>      LaunchpadZopelessLayer,
> @@ -86,6 +91,10 @@
>      return bmp, source_rev_id, target_rev_id
>  
>  
> +class FakeGitHostingClient:
> +    pass
> +
> +
>  class DiffTestCase(TestCaseWithFactory):
>  
>      def createExampleMerge(self):
> @@ -128,6 +137,44 @@
>          return (source_bzr, source_rev_id, target_bzr, prerequisite_bzr,
>                  prerequisite)
>  
> +    def installFakeGitMergeDiff(self, result=None, failure=None):
> +        hosting_client = FakeGitHostingClient()
> +        hosting_client.getMergeDiff = FakeMethod(
> +            result=result, failure=failure)
> +        self.useFixture(MonkeyPatch(
> +            "lp.code.model.diff.PreviewDiff.getGitHostingClient",
> +            staticmethod(lambda: hosting_client)))
> +
> +    def createExampleGitMerge(self):
> +        """Create an example Git-based merge scenario.
> +
> +        The actual diff work is done in turnip, and we have no turnip
> +        fixture as yet, so for the moment we just install a fake hosting
> +        client that will return a plausible-looking diff.
> +        """
> +        bmp = self.factory.makeBranchMergeProposalForGit()
> +        source_sha1 = bmp.source_git_ref.commit_sha1
> +        target_sha1 = bmp.target_git_ref.commit_sha1
> +        patch = dedent("""\
> +            diff --git a/foo b/foo
> +            index %s..%s 100644
> +            --- a/foo
> +            +++ b/foo
> +            @@ -1,2 +1,7 @@
> +            +<<<<<<< foo
> +             c
> +            +=======
> +            +d
> +            +>>>>>>> foo
> +             a
> +            +b
> +            """) % (target_sha1[:7], source_sha1[:7])
> +        self.installFakeGitMergeDiff(result={
> +            "patch": patch,
> +            "conflicts": ["foo"],
> +            })
> +        return bmp, source_sha1, target_sha1, patch
> +
>  
>  class TestDiff(DiffTestCase):
>  
> @@ -329,6 +376,10 @@
>  
>      layer = LaunchpadFunctionalLayer
>  
> +    def setUp(self):
> +        super(TestPreviewDiff, self).setUp()
> +        self.useFixture(FeatureFixture({GIT_FEATURE_FLAG: u"on"}))
> +
>      def _createProposalWithPreviewDiff(self, prerequisite_branch=None,
>                                         content=None):
>          # Create and return a preview diff.
> @@ -348,6 +399,24 @@
>          transaction.commit()
>          return mp
>  
> +    def _createGitProposalWithPreviewDiff(self, merge_prerequisite=None,
> +                                          content=None):
> +        mp = self.factory.makeBranchMergeProposalForGit(
> +            prerequisite_ref=merge_prerequisite)
> +        login_person(mp.registrant)
> +        if merge_prerequisite is None:
> +            prerequisite_revision_id = None
> +        else:
> +            prerequisite_revision_id = u"c" * 40
> +        if content is None:
> +            content = "".join(unified_diff("", "content"))
> +        mp.updatePreviewDiff(
> +            content, u"a" * 40, u"b" * 40,
> +            prerequisite_revision_id=prerequisite_revision_id)
> +        # Make sure the librarian file is written.
> +        transaction.commit()
> +        return mp
> +
>      def test_providesInterface(self):
>          # In order to test the interface provision, we need to make sure that
>          # the associated diff object that is delegated to is also created.
> @@ -365,7 +434,7 @@
>  
>      def test_title(self):
>          # PreviewDiff has title and it copes with absent
> -        # BranchRevision{.sequence} when branches get overridden/rebased. 
> +        # BranchRevision{.sequence} when branches get overridden/rebased.
>          mp = self._createProposalWithPreviewDiff(content='')
>          preview = mp.preview_diff
>          # Both, source and target, branch revisions are absent,
> @@ -386,6 +455,12 @@
>          target_rev.sequence = 1
>          self.assertEqual('r10 into r1', preview.title)
>  
> +    def test_title_git(self):
> +        # Git commit IDs are shortened to seven characters.
> +        mp = self._createGitProposalWithPreviewDiff(content='')
> +        preview = mp.preview_diff
> +        self.assertEqual('aaaaaaa into bbbbbbb', preview.title)
> +
>      def test_empty_diff(self):
>          # Once the source is merged into the target, the diff between the
>          # branches will be empty.
> @@ -495,6 +570,24 @@
>          finally:
>              logger.removeHandler(handler)
>  
> +    def test_fromBranchMergeProposalForGit(self):
> +        # Correctly generates a PreviewDiff from a Git-based
> +        # BranchMergeProposal.
> +        bmp, source_rev_id, target_rev_id, patch = self.createExampleGitMerge()
> +        preview = PreviewDiff.fromBranchMergeProposal(bmp)
> +        self.assertEqual(source_rev_id, preview.source_revision_id)
> +        self.assertEqual(target_rev_id, preview.target_revision_id)
> +        transaction.commit()
> +        self.assertEqual(patch, preview.text)
> +        self.assertEqual({'foo': (5, 0)}, preview.diffstat)
> +
> +    def test_fromBranchMergeProposalForGit_sets_conflicts(self):
> +        # Conflicts are set on the PreviewDiff.
> +        bmp, source_rev_id, target_rev_id, _ = self.createExampleGitMerge()
> +        preview = PreviewDiff.fromBranchMergeProposal(bmp)
> +        self.assertEqual('Conflict in foo\n', preview.conflicts)
> +        self.assertTrue(preview.has_conflicts)
> +
>      def test_getFileByName(self):
>          diff = self._createProposalWithPreviewDiff().preview_diff
>          self.assertEqual(diff.diff_text, diff.getFileByName('preview.diff'))
> 


-- 
https://code.launchpad.net/~cjwatson/launchpad/git-mp-diffs/+merge/257768
Your team Launchpad code reviewers is subscribed to branch lp:launchpad.


References