← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~abentley/launchpad/incremental-diffs into lp:launchpad

 

Aaron Bentley has proposed merging lp:~abentley/launchpad/incremental-diffs into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)


= Summary =
Implement incremental diff generation

== Proposed fix ==
See above

== Pre-implementation notes ==
Mid-implementation with thumper

== Implementation details ==
The generation of incremental diffs is handled by the Difftacular bzr plugin,
and the full testing of its behaviour is provided by its test suite.

I implemented the diff_changes test method to get a clearer idea what was added
and removed by a given diff.

== Tests ==
bin/test -t TestIncrementalDiff -t TestBranchMergeProposalGetIncrementalDiffs

== Demo and Q/A ==
None

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/code/configure.zcml
  lib/lp/code/model/branchmergeproposal.py
  lib/lp/code/model/tests/test_branchmergeproposal.py
  lib/lp/code/model/diff.py
  lib/lp/testing/factory.py
  lib/lp/code/model/tests/test_diff.py
  utilities/sourcedeps.conf
  lib/lp/code/interfaces/diff.py

./lib/lp/code/model/tests/test_branchmergeproposal.py
     192: E231 missing whitespace after ','
     194: E231 missing whitespace after ','
./lib/lp/code/model/diff.py
     166: E301 expected 1 blank line, found 0
-- 
https://code.launchpad.net/~abentley/launchpad/incremental-diffs/+merge/36875
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~abentley/launchpad/incremental-diffs into lp:launchpad.
=== added symlink 'bzrplugins/difftacular'
=== target is u'../sourcecode/difftacular/'
=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml	2010-09-21 14:54:43 +0000
+++ lib/lp/code/configure.zcml	2010-09-28 14:47:44 +0000
@@ -233,6 +233,7 @@
                     review_diff
                     next_preview_diff_job
                     preview_diff
+                    getIncrementalDiffs
                     votes
                     all_comments
                     related_bugs
@@ -245,7 +246,8 @@
                     isValidTransition
                     getUnlandedSourceBranchRevisions
                     root_message_id
-                    getUsersVoteReference"/>
+                    getUsersVoteReference
+                    generateIncrementalDiff"/>
     <allow interface="lp.code.interfaces.branchtarget.IHasBranchTarget"/>
     <require
         permission="launchpad.Edit"
@@ -897,6 +899,10 @@
   <class class="lp.code.model.diff.Diff">
     <allow interface="lp.code.interfaces.diff.IDiff" />
   </class>
+  <class class="lp.code.model.diff.IncrementalDiff">
+    <allow interface="lp.code.interfaces.diff.IDiff" />
+    <allow interface="lp.code.interfaces.diff.IIncrementalDiff" />
+  </class>
   <class class="lp.code.model.diff.StaticDiff">
     <allow interface="lp.code.interfaces.diff.IStaticDiff" />
   </class>

=== modified file 'lib/lp/code/interfaces/diff.py'
--- lib/lp/code/interfaces/diff.py	2010-08-20 20:31:18 +0000
+++ lib/lp/code/interfaces/diff.py	2010-09-28 14:47:44 +0000
@@ -9,6 +9,7 @@
 
 __all__ = [
     'IDiff',
+    'IIncrementalDiff',
     'IPreviewDiff',
     'IStaticDiff',
     'IStaticDiffSource',
@@ -29,6 +30,7 @@
     )
 
 from canonical.launchpad import _
+from lp.code.interfaces.revision import IRevision
 
 
 class IDiff(Interface):
@@ -62,6 +64,29 @@
             readonly=True))
 
 
+class IIncrementalDiff(Interface):
+    """An incremental diff for a merge proposal."""
+
+    diff = exported(
+        Reference(IDiff, title=_('The Diff object.'), readonly=True))
+
+    # The schema for the Reference gets patched in _schema_circular_imports.
+    branch_merge_proposal = exported(
+        Reference(
+            Interface, readonly=True,
+            title=_('The branch merge proposal that diff relates to.')))
+
+    old_revision = exported(
+        Reference(
+            IRevision, readonly=True,
+            title=_('The old revision of the diff.')))
+
+    new_revision = exported(
+        Reference(
+            IRevision, readonly=True,
+            title=_('The new revision of the diff.')))
+
+
 class IStaticDiff(Interface):
     """A diff with a fixed value, i.e. between two revisions."""
 

=== modified file 'lib/lp/code/model/branchmergeproposal.py'
--- lib/lp/code/model/branchmergeproposal.py	2010-09-24 22:30:48 +0000
+++ lib/lp/code/model/branchmergeproposal.py	2010-09-28 14:47:44 +0000
@@ -50,6 +50,7 @@
     SQLBase,
     sqlvalues,
     )
+from canonical.launchpad.interfaces.lpstorm import IMasterStore
 from lp.code.enums import (
     BranchMergeProposalStatus,
     CodeReviewVote,
@@ -77,7 +78,7 @@
 from lp.code.model.branchrevision import BranchRevision
 from lp.code.model.codereviewcomment import CodeReviewComment
 from lp.code.model.codereviewvote import CodeReviewVoteReference
-from lp.code.model.diff import PreviewDiff
+from lp.code.model.diff import Diff, IncrementalDiff, PreviewDiff
 from lp.registry.interfaces.person import (
     IPerson,
     validate_public_person,
@@ -777,6 +778,45 @@
         Store.of(self).flush()
         return self.preview_diff
 
+    def generateIncrementalDiff(self, old_revision, new_revision, diff=None):
+        """Generate an incremental diff for the merge proposal.
+
+        :param old_revision: The `Revision` to generate the diff from.
+        :param new_revision: The `Revision` to generate the diff to.
+        :param diff: If supplied, a pregenerated `Diff`.
+        """
+        if diff is None:
+            source_branch = self.source_branch.getBzrBranch()
+            ignore_branches = [self.target_branch.getBzrBranch()]
+            if self.prerequisite_branch is not None:
+                ignore_branches.append(
+                    self.prerequisite_branch.getBzrBranch())
+            diff = Diff.generateIncrementalDiff(
+                old_revision, new_revision, source_branch, ignore_branches)
+        incremental_diff = IncrementalDiff()
+        incremental_diff.diff = diff
+        incremental_diff.branch_merge_proposal = self
+        incremental_diff.old_revision = old_revision
+        incremental_diff.new_revision = new_revision
+        IMasterStore(IncrementalDiff).add(incremental_diff)
+        return incremental_diff
+
+    def getIncrementalDiffs(self, revision_list):
+        """Return a list of diffs for the specified revisions.
+
+        :param revision_list: A list of tuples of (`Revision`, `Revision`).
+            The first revision in the tuple is the old revision.  The second
+            is the new revision.
+        :return: A list of IncrementalDiffs in the same order as the supplied
+            Revisions.
+        """
+        diffs = Store.of(self).find(IncrementalDiff,
+            IncrementalDiff.branch_merge_proposal_id == self.id)
+        diff_dict = dict(
+            ((diff.old_revision, diff.new_revision), diff)
+            for diff in diffs)
+        return [diff_dict.get(revisions) for revisions in revision_list]
+
     @property
     def revision_end_date(self):
         """The cutoff date for showing revisions.

=== modified file 'lib/lp/code/model/diff.py'
--- lib/lp/code/model/diff.py	2010-08-20 20:31:18 +0000
+++ lib/lp/code/model/diff.py	2010-09-28 14:47:44 +0000
@@ -1,23 +1,26 @@
 # Copyright 2009 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
+from __future__ import with_statement
+
 """Implementation classes for IDiff, etc."""
 
 __metaclass__ = type
-__all__ = ['Diff', 'PreviewDiff', 'StaticDiff']
+__all__ = ['Diff', 'IncrementalDiff', 'PreviewDiff', 'StaticDiff']
 
 from cStringIO import StringIO
+from contextlib import nested
 import sys
 from uuid import uuid1
 
 from bzrlib import trace
-from bzrlib.branch import Branch
 from bzrlib.diff import show_diff_trees
 from bzrlib.merge import Merge3Merger
 from bzrlib.patches import (
     parse_patches,
     Patch,
     )
+from bzrlib.plugins.difftacular.generate_diff import diff_ignore_branches
 from lazr.delegates import delegates
 import simplejson
 from sqlobject import (
@@ -44,10 +47,12 @@
 from lp.app.errors import NotFoundError
 from lp.code.interfaces.diff import (
     IDiff,
+    IIncrementalDiff,
     IPreviewDiff,
     IStaticDiff,
     IStaticDiffSource,
     )
+from lp.codehosting.bzrutils import read_locked
 
 
 class Diff(SQLBase):
@@ -192,9 +197,14 @@
         diff_content = StringIO()
         show_diff_trees(from_tree, to_tree, diff_content, old_label='',
                         new_label='')
+        return klass.fromFileAtEnd(diff_content, filename)
+
+    @classmethod
+    def fromFileAtEnd(cls, diff_content, filename=None):
+        """Make a Diff from a file object that is currently at its end."""
         size = diff_content.tell()
         diff_content.seek(0)
-        return klass.fromFile(diff_content, size, filename)
+        return cls.fromFile(diff_content, size, filename)
 
     @classmethod
     def fromFile(cls, diff_content, size, filename=None):
@@ -255,6 +265,30 @@
             file_stats[path] = tuple(patch.stats_values()[:2])
         return file_stats
 
+    @classmethod
+    def generateIncrementalDiff(cls, old_revision, new_revision,
+            source_branch, ignore_branches):
+        """Return a Diff whose contents are an incremental diff.
+
+        The Diff's contents will show the changes made between old_revision
+        and new_revision, except those changes introduced by the
+        ignore_branches.
+
+        :param old_revision: The `Revision` to show changes from.
+        :param new_revision: The `Revision` to show changes to.
+        :param source_branch: The bzr branch containing these revisions.
+        :param ignore_brances: A collection of branches to ignore merges from.
+        :return: a `Diff`.
+        """
+        diff_content = StringIO()
+        read_locks = [read_locked(branch) for branch in [source_branch] +
+                ignore_branches]
+        with nested(*read_locks):
+            diff_ignore_branches(
+                source_branch, ignore_branches, old_revision.revision_id,
+                new_revision.revision_id, diff_content)
+        return cls.fromFileAtEnd(diff_content)
+
 
 class StaticDiff(SQLBase):
     """A diff from one revision to another."""
@@ -303,6 +337,36 @@
         diff.destroySelf()
 
 
+class IncrementalDiff(Storm):
+    """See `IIncrementalDiff."""
+
+    implements(IIncrementalDiff)
+
+    delegates(IDiff, context='diff')
+
+    __storm_table__ = 'IncrementalDiff'
+
+    id = Int(primary=True, allow_none=False)
+
+    diff_id = Int(name='diff', allow_none=False)
+
+    diff = Reference(diff_id, 'Diff.id')
+
+    branch_merge_proposal_id = Int(
+        name='branch_merge_proposal', allow_none=False)
+
+    branch_merge_proposal = Reference(
+        branch_merge_proposal_id, "BranchMergeProposal.id")
+
+    old_revision_id = Int(name='old_revision', allow_none=False)
+
+    old_revision = Reference(old_revision_id, 'Revision.id')
+
+    new_revision_id = Int(name='new_revision', allow_none=False)
+
+    new_revision = Reference(new_revision_id, 'Revision.id')
+
+
 class PreviewDiff(Storm):
     """See `IPreviewDiff`."""
     implements(IPreviewDiff)

=== modified file 'lib/lp/code/model/tests/test_branchmergeproposal.py'
--- lib/lp/code/model/tests/test_branchmergeproposal.py	2010-09-15 20:41:46 +0000
+++ lib/lp/code/model/tests/test_branchmergeproposal.py	2010-09-28 14:47:44 +0000
@@ -1862,5 +1862,45 @@
         self.assertRevisionGroups(bmp, expected_groups)
 
 
+class TestBranchMergeProposalGetIncrementalDiffs(TestCaseWithFactory):
+
+    layer = LaunchpadFunctionalLayer
+
+    def test_getIncrementalDiffs(self):
+        """getIncrementalDiffs returns the requested values or None.
+
+        None is returned if there is no IncrementalDiff for the requested
+        revision pair and branch_merge_proposal."""
+        bmp = self.factory.makeBranchMergeProposal()
+        diff1 = self.factory.makeIncrementalDiff(merge_proposal=bmp)
+        diff2 = self.factory.makeIncrementalDiff(merge_proposal=bmp)
+        diff3 = self.factory.makeIncrementalDiff()
+        result = bmp.getIncrementalDiffs([
+            (diff1.old_revision, diff1.new_revision),
+            (diff2.old_revision, diff2.new_revision),
+            # Wrong merge proposal
+            (diff3.old_revision, diff3.new_revision),
+            # Mismatched revisions
+            (diff1.old_revision, diff2.new_revision),
+        ])
+        self.assertEqual([diff1, diff2, None, None], result)
+
+    def test_getIncrementalDiffs_respects_input_order(self):
+        """The order of the output follows the input order."""
+        bmp = self.factory.makeBranchMergeProposal()
+        diff1 = self.factory.makeIncrementalDiff(merge_proposal=bmp)
+        diff2 = self.factory.makeIncrementalDiff(merge_proposal=bmp)
+        result = bmp.getIncrementalDiffs([
+            (diff1.old_revision, diff1.new_revision),
+            (diff2.old_revision, diff2.new_revision),
+        ])
+        self.assertEqual([diff1, diff2], result)
+        result = bmp.getIncrementalDiffs([
+            (diff2.old_revision, diff2.new_revision),
+            (diff1.old_revision, diff1.new_revision),
+        ])
+        self.assertEqual([diff2, diff1], result)
+
+
 def test_suite():
     return TestLoader().loadTestsFromName(__name__)

=== modified file 'lib/lp/code/model/tests/test_diff.py'
--- lib/lp/code/model/tests/test_diff.py	2010-09-15 20:41:46 +0000
+++ lib/lp/code/model/tests/test_diff.py	2010-09-28 14:47:44 +0000
@@ -14,6 +14,7 @@
 from unittest import TestLoader
 
 from bzrlib import trace
+from bzrlib.patches import InsertLine, parse_patches, RemoveLine
 import transaction
 from zope.security.proxy import removeSecurityProxy
 
@@ -26,6 +27,7 @@
 from lp.app.errors import NotFoundError
 from lp.code.interfaces.diff import (
     IDiff,
+    IIncrementalDiff,
     IPreviewDiff,
     IStaticDiff,
     IStaticDiffSource,
@@ -523,5 +525,69 @@
             NotFoundError, diff.getFileByName, 'preview.diff')
 
 
+class TestIncrementalDiff(DiffTestCase):
+    """Test that IncrementalDiff objects work."""
+
+    layer = LaunchpadFunctionalLayer
+
+    def test_providesInterface(self):
+        incremental_diff = self.factory.makeIncrementalDiff()
+        verifyObject(IIncrementalDiff, incremental_diff)
+
+    @staticmethod
+    def diff_changes(diff_bytes):
+        inserted = []
+        removed = []
+        for patch in parse_patches(diff_bytes.splitlines(True)):
+            for hunk in patch.hunks:
+                for line in hunk.lines:
+                    if isinstance(line, InsertLine):
+                        inserted.append(line.contents)
+                    if isinstance(line, RemoveLine):
+                        removed.append(line.contents)
+        return inserted, removed
+
+    def test_generateIncrementalDiff(self):
+        """generateIncrementalDiff works.
+
+        Changes merged from the prerequisite and target are ignored in the
+        diff.
+        """
+        self.useBzrBranches(direct_database=True)
+        prerequisite_branch = self.factory.makeAnyBranch()
+        bmp = self.factory.makeBranchMergeProposal(
+            prerequisite_branch=prerequisite_branch)
+        target_branch = self.createBzrBranch(bmp.target_branch)
+        old_revision_id = self.commitFile(
+            bmp.target_branch, 'foo', 'a\nb\ne\n')
+        old_revision = self.factory.makeRevision(rev_id=old_revision_id)
+        source_branch = self.createBzrBranch(
+            bmp.source_branch, target_branch)
+        self.commitFile(
+            bmp.source_branch, 'foo', 'a\nc\ne\n')
+        prerequisite = self.createBzrBranch(
+            bmp.prerequisite_branch, target_branch)
+        prerequisite_revision = self.commitFile(bmp.prerequisite_branch,
+            'foo', 'd\na\nb\ne\n')
+        merge_parent = self.commitFile(bmp.target_branch, 'foo',
+            'a\nb\ne\nf\n')
+        source_branch.repository.fetch(target_branch.repository,
+            revision_id=merge_parent)
+        self.commitFile(
+            bmp.source_branch, 'foo', 'a\nc\ne\nf\n', [merge_parent])
+        source_branch.repository.fetch(prerequisite.repository,
+            revision_id=prerequisite_revision)
+        new_revision_id = self.commitFile(
+            bmp.source_branch, 'foo', 'd\na\nc\ne\nf\n',
+            [prerequisite_revision])
+        new_revision = self.factory.makeRevision(rev_id=new_revision_id)
+        incremental_diff = bmp.generateIncrementalDiff(
+            old_revision, new_revision)
+        transaction.commit()
+        inserted, removed = self.diff_changes(incremental_diff.text)
+        self.assertEqual(['c\n'], inserted)
+        self.assertEqual(['b\n'], removed)
+
+
 def test_suite():
     return TestLoader().loadTestsFromName(__name__)

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2010-09-23 19:52:50 +0000
+++ lib/lp/testing/factory.py	2010-09-28 14:47:44 +0000
@@ -1238,6 +1238,19 @@
         preview_diff.target_revision_id = self.getUniqueUnicode()
         return preview_diff
 
+    def makeIncrementalDiff(self, merge_proposal=None, old_revision=None,
+                            new_revision=None):
+        diff = self.makeDiff()
+        if merge_proposal is None:
+            merge_proposal = self.makeBranchMergeProposal()
+        if old_revision is None:
+            old_revision = self.makeRevision()
+        if new_revision is None:
+            new_revision = self.makeRevision()
+        diff = self.makeDiff()
+        return merge_proposal.generateIncrementalDiff(
+            old_revision, new_revision, diff)
+
     def makeStaticDiff(self):
         return StaticDiff.acquireFromText(
             self.getUniqueUnicode(), self.getUniqueUnicode(),

=== modified file 'utilities/sourcedeps.conf'
--- utilities/sourcedeps.conf	2010-09-27 13:59:38 +0000
+++ utilities/sourcedeps.conf	2010-09-28 14:47:44 +0000
@@ -5,6 +5,7 @@
 bzr-svn lp:~launchpad-pqm/bzr-svn/devel;revno=2710
 cscvs lp:~launchpad-pqm/launchpad-cscvs/devel;revno=432
 dulwich lp:~launchpad-pqm/dulwich/devel;revno=424
+difftacular lp:difftacular;revno=6
 loggerhead lp:~launchpad-pqm/loggerhead/devel;revno=176
 lpreview lp:~launchpad-pqm/bzr-lpreview/devel;revno=23
 mailman lp:~launchpad-pqm/mailman/2.1;revno=976