← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~abentley/launchpad/incremental-diff-driveby into lp:launchpad/devel

 

Aaron Bentley has proposed merging lp:~abentley/launchpad/incremental-diff-driveby into lp:launchpad/devel.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)


= Summary =
A set of drivebys to support incremental diffs.  Although incremental diffs
will be merged into db-devel, the drivebys will land in devel to reduce skew.

== Proposed fix ==
N/A

== Pre-implementation notes ==
Moving view code into the model was discussed with thumper.

== Implementation details ==
revision_end_date and getRevisionsSinceReviewStart are moved from the view to
the model.  getRevisionsSinceReviewStart uses itertools.groupby instead of
reimplementing it.  _getNewerRevisions is extracted from
getRevisionsSinceReviewStart.

bzrutils.read_locked allows branches to be locked using with statement.

DirectBranchCommit allows merge parents to be specified.
DiffTestCase.commitFile allows merge parents to be specified.

add_revision_to_branch produces realistic parent references.

Revision.getLefthandParent provides convenient access to the lefthand parent of
the revision.

Literal tab characters are replaced with \t to please lint.

== Tests ==
bin/test -v -t test_commit_uses_merge_parents -t TestGetRevisionsSinceReviewStart -t TestRevisionEndDate

== 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/interfaces/branchmergeproposal.py
  lib/lp/code/model/branchmergeproposal.py
  lib/lp/code/model/tests/test_branchmergeproposal.py
  lib/lp/code/tests/helpers.py
  lib/lp/code/browser/tests/test_branchmergeproposal.py
  lib/lp/code/model/directbranchcommit.py
  lib/lp/codehosting/bzrutils.py
  lib/lp/code/interfaces/revision.py
  lib/lp/code/model/tests/test_diff.py
  lib/lp/code/browser/branchmergeproposal.py
  lib/lp/code/tests/test_directbranchcommit.py
  lib/lp/code/model/revision.py

./lib/lp/code/interfaces/branchmergeproposal.py
     715: E302 expected 2 blank lines, found 1
./lib/lp/code/model/tests/test_branchmergeproposal.py
     192: E231 missing whitespace after ','
     194: E231 missing whitespace after ','
./lib/lp/codehosting/bzrutils.py
     165: E301 expected 1 blank line, found 0
     343: E301 expected 1 blank line, found 0
./lib/lp/code/browser/branchmergeproposal.py
     168: E301 expected 1 blank line, found 0
     178: E301 expected 1 blank line, found 0
     201: E202 whitespace before '}'
    1441: E301 expected 1 blank line, found 0
-- 
https://code.launchpad.net/~abentley/launchpad/incremental-diff-driveby/+merge/36156
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~abentley/launchpad/incremental-diff-driveby into lp:launchpad/devel.
=== modified file 'lib/lp/code/browser/branchmergeproposal.py'
--- lib/lp/code/browser/branchmergeproposal.py	2010-08-24 10:45:57 +0000
+++ lib/lp/code/browser/branchmergeproposal.py	2010-09-21 15:06:00 +0000
@@ -32,7 +32,6 @@
     'latest_proposals_for_each_branch',
     ]
 
-from collections import defaultdict
 import operator
 
 from lazr.delegates import delegates
@@ -200,7 +199,7 @@
                 'Approved [Merge Failed]',
             BranchMergeProposalStatus.QUEUED : 'Queued',
             BranchMergeProposalStatus.SUPERSEDED : 'Superseded'
-            }
+        }
         return friendly_texts[self.context.queue_status]
 
     @property
@@ -212,8 +211,7 @@
         result = ''
         if self.context.queue_status in (
             BranchMergeProposalStatus.CODE_APPROVED,
-            BranchMergeProposalStatus.REJECTED
-            ):
+            BranchMergeProposalStatus.REJECTED):
             formatter = DateTimeFormatterAPI(self.context.date_reviewed)
             result = '%s %s' % (
                 self.context.reviewer.displayname,
@@ -601,46 +599,15 @@
         """Location of page for commenting on this proposal."""
         return canonical_url(self.context, view_name='+comment')
 
-    @property
-    def revision_end_date(self):
-        """The cutoff date for showing revisions.
-
-        If the proposal has been merged, then we stop at the merged date. If
-        it is rejected, we stop at the reviewed date. For superseded
-        proposals, it should ideally use the non-existant date_last_modified,
-        but could use the last comment date.
-        """
-        status = self.context.queue_status
-        if status == BranchMergeProposalStatus.MERGED:
-            return self.context.date_merged
-        if status == BranchMergeProposalStatus.REJECTED:
-            return self.context.date_reviewed
-        # Otherwise return None representing an open end date.
-        return None
-
-    def _getRevisionsSinceReviewStart(self):
-        """Get the grouped revisions since the review started."""
-        # Work out the start of the review.
-        start_date = self.context.date_review_requested
-        if start_date is None:
-            start_date = self.context.date_created
-        source = DecoratedBranch(self.context.source_branch)
-        resultset = source.getMainlineBranchRevisions(
-            start_date, self.revision_end_date, oldest_first=True)
-        # Now group by date created.
-        groups = defaultdict(list)
-        for branch_revision, revision, revision_author in resultset:
-            groups[revision.date_created].append(branch_revision)
-        return [
-            CodeReviewNewRevisions(revisions, date, source)
-            for date, revisions in groups.iteritems()]
-
     @cachedproperty
     def conversation(self):
         """Return a conversation that is to be rendered."""
         # Sort the comments by date order.
-        comments = self._getRevisionsSinceReviewStart()
         merge_proposal = self.context
+        groups = merge_proposal.getRevisionsSinceReviewStart()
+        source = DecoratedBranch(merge_proposal.source_branch)
+        comments = [CodeReviewNewRevisions(list(revisions), date, source)
+            for date, revisions in groups]
         while merge_proposal is not None:
             from_superseded = merge_proposal != self.context
             comments.extend(
@@ -946,7 +913,6 @@
         self.cancel_url = self.next_url
         super(MergeProposalEditView, self).initialize()
 
-
     def _getRevisionId(self, data):
         """Translate the revision number that was entered into a revision id.
 
@@ -1473,8 +1439,8 @@
     """Render an `IText` as XHTML using the webservice."""
     formatter = FormattersAPI
     def renderer(value):
-        nomail  = formatter(value).obfuscate_email()
-        html    = formatter(nomail).text_to_html()
+        nomail = formatter(value).obfuscate_email()
+        html = formatter(nomail).text_to_html()
         return html.encode('utf-8')
     return renderer
 

=== modified file 'lib/lp/code/browser/tests/test_branchmergeproposal.py'
--- lib/lp/code/browser/tests/test_branchmergeproposal.py	2010-08-22 21:34:16 +0000
+++ lib/lp/code/browser/tests/test_branchmergeproposal.py	2010-09-21 15:06:00 +0000
@@ -12,7 +12,6 @@
     timedelta,
     )
 from difflib import unified_diff
-import operator
 import unittest
 
 import pytz
@@ -46,7 +45,6 @@
     PreviewDiff,
     StaticDiff,
     )
-from lp.code.tests.helpers import add_revision_to_branch
 from lp.testing import (
     login_person,
     TestCaseWithFactory,
@@ -577,63 +575,6 @@
         view = create_initialized_view(self.bmp, '+index')
         self.assertEqual([], view.linked_bugs)
 
-    def test_revision_end_date_active(self):
-        # An active merge proposal will have None as an end date.
-        bmp = self.factory.makeBranchMergeProposal()
-        view = create_initialized_view(bmp, '+index')
-        self.assertIs(None, view.revision_end_date)
-
-    def test_revision_end_date_merged(self):
-        # An merged proposal will have the date merged as an end date.
-        bmp = self.factory.makeBranchMergeProposal(
-            set_state=BranchMergeProposalStatus.MERGED)
-        view = create_initialized_view(bmp, '+index')
-        self.assertEqual(bmp.date_merged, view.revision_end_date)
-
-    def test_revision_end_date_rejected(self):
-        # An rejected proposal will have the date reviewed as an end date.
-        bmp = self.factory.makeBranchMergeProposal(
-            set_state=BranchMergeProposalStatus.REJECTED)
-        view = create_initialized_view(bmp, '+index')
-        self.assertEqual(bmp.date_reviewed, view.revision_end_date)
-
-    def assertRevisionGroups(self, bmp, expected_groups):
-        """Get the groups for the merge proposal and check them."""
-        view = create_initialized_view(bmp, '+index')
-        groups = view._getRevisionsSinceReviewStart()
-        view_groups = [
-            obj.revisions for obj in sorted(
-                groups, key=operator.attrgetter('date'))]
-        self.assertEqual(expected_groups, view_groups)
-
-    def test_getRevisionsSinceReviewStart_no_revisions(self):
-        # If there have been no revisions pushed since the start of the
-        # review, the method returns an empty list.
-        self.assertRevisionGroups(self.bmp, [])
-
-    def test_getRevisionsSinceReviewStart_groups(self):
-        # Revisions that were scanned at the same time have the same
-        # date_created.  These revisions are grouped together.
-        review_date = datetime(2009, 9, 10, tzinfo=pytz.UTC)
-        bmp = self.factory.makeBranchMergeProposal(
-            date_created=review_date)
-        login_person(bmp.registrant)
-        bmp.requestReview(review_date)
-        revision_date = review_date + timedelta(days=1)
-        revisions = []
-        for date in range(2):
-            revisions.append(
-                add_revision_to_branch(
-                    self.factory, bmp.source_branch, revision_date))
-            revisions.append(
-                add_revision_to_branch(
-                    self.factory, bmp.source_branch, revision_date))
-            revision_date += timedelta(days=1)
-        expected_groups = [
-            [revisions[0], revisions[1]],
-            [revisions[2], revisions[3]]]
-        self.assertRevisionGroups(bmp, expected_groups)
-
     def test_include_superseded_comments(self):
         for x, time in zip(range(3), time_counter()):
             if x != 0:
@@ -757,7 +698,6 @@
 
     layer = LaunchpadFunctionalLayer
 
-
     def _makeCommentFromEmailWithAttachment(self, attachment_body):
         # Make an email message with an attachment, and create a code
         # review comment from it.

=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml	2010-09-20 22:16:32 +0000
+++ lib/lp/code/configure.zcml	2010-09-21 15:06:00 +0000
@@ -236,8 +236,10 @@
                     votes
                     all_comments
                     related_bugs
+                    revision_end_date
                     isMergable
                     getComment
+                    getRevisionsSinceReviewStart
                     getNotificationRecipients
                     getVoteReference
                     isValidTransition

=== modified file 'lib/lp/code/interfaces/branchmergeproposal.py'
--- lib/lp/code/interfaces/branchmergeproposal.py	2010-08-20 20:31:18 +0000
+++ lib/lp/code/interfaces/branchmergeproposal.py	2010-09-21 15:06:00 +0000
@@ -290,6 +290,13 @@
     def getComment(id):
         """Return the CodeReviewComment with the specified ID."""
 
+    def getRevisionsSinceReviewStart():
+        """Return all the revisions added since the review began.
+
+        Revisions are grouped by creation (i.e. push) time.
+        :return: An iterator of (date, iterator of revision data)
+        """
+
     def getVoteReference(id):
         """Return the CodeReviewVoteReference with the specified ID."""
 
@@ -518,8 +525,8 @@
             source branch.
         :param target_revision_id: The revision id that was used from the
             target branch.
-        :param prerequisite_revision_id: The revision id that was used from the
-            prerequisite branch.
+        :param prerequisite_revision_id: The revision id that was used from
+            the prerequisite branch.
         :param conflicts: Text describing the conflicts if any.
         """
 

=== modified file 'lib/lp/code/interfaces/revision.py'
--- lib/lp/code/interfaces/revision.py	2010-08-20 20:31:18 +0000
+++ lib/lp/code/interfaces/revision.py	2010-09-21 15:06:00 +0000
@@ -73,6 +73,9 @@
         :return: A `Branch` or None if an appropriate branch cannot be found.
         """
 
+    def getLefthandParent():
+        """Return lefthand parent of revision, or None if not in database."""
+
 
 class IRevisionAuthor(Interface):
     """Committer of a Bazaar revision."""

=== modified file 'lib/lp/code/model/branchmergeproposal.py'
--- lib/lp/code/model/branchmergeproposal.py	2010-08-20 20:31:18 +0000
+++ lib/lp/code/model/branchmergeproposal.py	2010-09-21 15:06:00 +0000
@@ -13,7 +13,7 @@
     ]
 
 from email.Utils import make_msgid
-
+from itertools import groupby
 from sqlobject import (
     ForeignKey,
     IntCol,
@@ -777,6 +777,45 @@
         Store.of(self).flush()
         return self.preview_diff
 
+    @property
+    def revision_end_date(self):
+        """The cutoff date for showing revisions.
+
+        If the proposal has been merged, then we stop at the merged date. If
+        it is rejected, we stop at the reviewed date. For superseded
+        proposals, it should ideally use the non-existant date_last_modified,
+        but could use the last comment date.
+        """
+        status = self.queue_status
+        if status == BranchMergeProposalStatus.MERGED:
+            return self.date_merged
+        if status == BranchMergeProposalStatus.REJECTED:
+            return self.date_reviewed
+        # Otherwise return None representing an open end date.
+        return None
+
+    def _getNewerRevisions(self):
+        start_date = self.date_review_requested
+        if start_date is None:
+            start_date = self.date_created
+        return self.source_branch.getMainlineBranchRevisions(
+            start_date, self.revision_end_date, oldest_first=True)
+
+    def getRevisionsSinceReviewStart(self):
+        """Get the grouped revisions since the review started."""
+        resultset = self._getNewerRevisions()
+        # Work out the start of the review.
+        branch_revisions = (
+            branch_revision for branch_revision, revision, revision_author
+            in resultset)
+        # Now group by date created.
+        gby = groupby(branch_revisions, lambda r: r.revision.date_created)
+        # Use a generator expression to wrap the custom iterator so it doesn't
+        # get security-proxied.
+        return (
+            (date, (revision for revision in revisions))
+            for date, revisions in gby)
+
 
 class BranchMergeProposalGetter:
     """See `IBranchMergeProposalGetter`."""

=== modified file 'lib/lp/code/model/directbranchcommit.py'
--- lib/lp/code/model/directbranchcommit.py	2010-08-20 20:31:18 +0000
+++ lib/lp/code/model/directbranchcommit.py	2010-09-21 15:06:00 +0000
@@ -55,7 +55,8 @@
     is_locked = False
     commit_builder = None
 
-    def __init__(self, db_branch, committer=None, no_race_check=False):
+    def __init__(self, db_branch, committer=None, no_race_check=False,
+            merge_parents=None):
         """Create context for direct commit to branch.
 
         Before constructing a `DirectBranchCommit`, set up a server that
@@ -107,6 +108,7 @@
             raise
 
         self.files = set()
+        self.merge_parents = merge_parents
 
     def _getDir(self, path):
         """Get trans_id for directory "path."  Create if necessary."""
@@ -200,7 +202,8 @@
             # required to generate the revision-id.
             with override_environ(BZR_EMAIL=committer_id):
                 new_rev_id = self.transform_preview.commit(
-                    self.bzrbranch, commit_message, committer=committer_id)
+                    self.bzrbranch, commit_message, self.merge_parents,
+                    committer=committer_id)
             IMasterObject(self.db_branch).branchChanged(
                 get_stacked_on_url(self.bzrbranch), new_rev_id,
                 self.db_branch.control_format, self.db_branch.branch_format,

=== modified file 'lib/lp/code/model/revision.py'
--- lib/lp/code/model/revision.py	2010-08-27 02:11:36 +0000
+++ lib/lp/code/model/revision.py	2010-09-21 15:06:00 +0000
@@ -18,6 +18,7 @@
     )
 import email
 
+from bzrlib.revision import NULL_REVISION
 import pytz
 from sqlobject import (
     BoolCol,
@@ -39,7 +40,6 @@
     )
 from storm.locals import (
     Bool,
-    DateTime,
     Int,
     Min,
     Reference,
@@ -118,6 +118,13 @@
         """
         return [parent.parent_id for parent in self.parents]
 
+    def getLefthandParent(self):
+        if len(self.parent_ids) == 0:
+            parent_id = NULL_REVISION
+        else:
+            parent_id = self.parent_ids[0]
+        return RevisionSet().getByRevisionId(parent_id)
+
     def getProperties(self):
         """See `IRevision`."""
         return dict((prop.name, prop.value) for prop in self.properties)

=== modified file 'lib/lp/code/model/tests/test_branchmergeproposal.py'
--- lib/lp/code/model/tests/test_branchmergeproposal.py	2010-08-20 20:31:18 +0000
+++ lib/lp/code/model/tests/test_branchmergeproposal.py	2010-09-21 15:06:00 +0000
@@ -7,7 +7,7 @@
 
 __metaclass__ = type
 
-from datetime import datetime
+from datetime import datetime, timedelta
 from difflib import unified_diff
 from unittest import (
     TestCase,
@@ -72,6 +72,7 @@
     MergeProposalCreatedJob,
     UpdatePreviewDiffJob,
     )
+from lp.code.tests.helpers import add_revision_to_branch
 from lp.registry.interfaces.person import IPersonSet
 from lp.registry.interfaces.product import IProductSet
 from lp.testing import (
@@ -108,7 +109,8 @@
         self.assertTrue(url.startswith(source_branch_url))
 
     def test_BranchMergeProposal_canonical_url_rest(self):
-        # The rest of the URL for a merge proposal is +merge followed by the db id.
+        # The rest of the URL for a merge proposal is +merge followed by the
+        # db id.
         bmp = self.factory.makeBranchMergeProposal()
         url = canonical_url(bmp)
         source_branch_url = canonical_url(bmp.source_branch)
@@ -238,7 +240,6 @@
                           self._attemptTransition,
                           proposal, to_state)
 
-
     def assertGoodDupeTransition(self, from_state, to_state):
         """Trying to go from `from_state` to `to_state` succeeds."""
         proposal = self.prepareDupeTransition(from_state)
@@ -1049,6 +1050,7 @@
             else:
                 self.assertEqual([mp], list(active))
 
+
 class TestBranchMergeProposalGetterGetProposals(TestCaseWithFactory):
     """Test the getProposalsForContext method."""
 
@@ -1118,7 +1120,6 @@
             beaver, [BranchMergeProposalStatus.REJECTED], beaver)
         self.assertEqual(beave_proposals.count(), 1)
 
-
     def test_created_proposal_default_status(self):
         # When we create a merge proposal using the helper method, the default
         # status of the proposal is work in progress.
@@ -1799,5 +1800,67 @@
         self.assertIs(None, bmp.next_preview_diff_job)
 
 
+class TestRevisionEndDate(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_revision_end_date_active(self):
+        # An active merge proposal will have None as an end date.
+        bmp = self.factory.makeBranchMergeProposal()
+        self.assertIs(None, bmp.revision_end_date)
+
+    def test_revision_end_date_merged(self):
+        # An merged proposal will have the date merged as an end date.
+        bmp = self.factory.makeBranchMergeProposal(
+            set_state=BranchMergeProposalStatus.MERGED)
+        self.assertEqual(bmp.date_merged, bmp.revision_end_date)
+
+    def test_revision_end_date_rejected(self):
+        # An rejected proposal will have the date reviewed as an end date.
+        bmp = self.factory.makeBranchMergeProposal(
+            set_state=BranchMergeProposalStatus.REJECTED)
+        self.assertEqual(bmp.date_reviewed, bmp.revision_end_date)
+
+
+class TestGetRevisionsSinceReviewStart(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def assertRevisionGroups(self, bmp, expected_groups):
+        """Get the groups for the merge proposal and check them."""
+        groups = bmp.getRevisionsSinceReviewStart()
+        revision_groups = [list(revisions) for date, revisions in groups]
+        self.assertEqual(expected_groups, revision_groups)
+
+    def test_getRevisionsSinceReviewStart_no_revisions(self):
+        # If there have been no revisions pushed since the start of the
+        # review, the method returns an empty list.
+        bmp = self.factory.makeBranchMergeProposal()
+        self.assertRevisionGroups(bmp, [])
+
+    def test_getRevisionsSinceReviewStart_groups(self):
+        # Revisions that were scanned at the same time have the same
+        # date_created.  These revisions are grouped together.
+        review_date = datetime(2009, 9, 10, tzinfo=UTC)
+        bmp = self.factory.makeBranchMergeProposal(
+            date_created=review_date)
+        login_person(bmp.registrant)
+        bmp.requestReview(review_date)
+        revision_date = review_date + timedelta(days=1)
+        revisions = []
+        for date in range(2):
+            revisions.append(
+                add_revision_to_branch(
+                    self.factory, bmp.source_branch, revision_date))
+            revisions.append(
+                add_revision_to_branch(
+                    self.factory, bmp.source_branch, revision_date))
+            revision_date += timedelta(days=1)
+        expected_groups = [
+            [revisions[0], revisions[1]],
+            [revisions[2], revisions[3]]]
+        self.assertRevisionGroups(bmp, expected_groups)
+
+
 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-08-20 20:31:18 +0000
+++ lib/lp/code/model/tests/test_diff.py	2010-09-21 15:06:00 +0000
@@ -57,13 +57,14 @@
 class DiffTestCase(TestCaseWithFactory):
 
     @staticmethod
-    def commitFile(branch, path, contents):
+    def commitFile(branch, path, contents, merge_parents=None):
         """Create a commit that updates a file to specified contents.
 
         This will create or modify the file, as needed.
         """
         committer = DirectBranchCommit(
-            removeSecurityProxy(branch), no_race_check=True)
+            removeSecurityProxy(branch), no_race_check=True,
+            merge_parents=merge_parents)
         committer.writeFile(path, contents)
         try:
             return committer.commit('committing')
@@ -122,7 +123,6 @@
                 prerequisite)
 
 
-
 class TestDiff(DiffTestCase):
 
     layer = LaunchpadFunctionalLayer
@@ -186,19 +186,19 @@
         self.checkExampleMerge(diff.text)
 
     diff_bytes = (
-        "--- bar	2009-08-26 15:53:34.000000000 -0400\n"
-        "+++ bar	1969-12-31 19:00:00.000000000 -0500\n"
+        "--- bar\t2009-08-26 15:53:34.000000000 -0400\n"
+        "+++ bar\t1969-12-31 19:00:00.000000000 -0500\n"
         "@@ -1,3 +0,0 @@\n"
         "-a\n"
         "-b\n"
         "-c\n"
-        "--- baz	1969-12-31 19:00:00.000000000 -0500\n"
-        "+++ baz	2009-08-26 15:53:57.000000000 -0400\n"
+        "--- baz\t1969-12-31 19:00:00.000000000 -0500\n"
+        "+++ baz\t2009-08-26 15:53:57.000000000 -0400\n"
         "@@ -0,0 +1,2 @@\n"
         "+a\n"
         "+b\n"
-        "--- foo	2009-08-26 15:53:23.000000000 -0400\n"
-        "+++ foo	2009-08-26 15:56:43.000000000 -0400\n"
+        "--- foo\t2009-08-26 15:53:23.000000000 -0400\n"
+        "+++ foo\t2009-08-26 15:56:43.000000000 -0400\n"
         "@@ -1,3 +1,4 @@\n"
         " a\n"
         "-b\n"
@@ -207,19 +207,19 @@
         "+e\n")
 
     diff_bytes_2 = (
-        "--- bar	2009-08-26 15:53:34.000000000 -0400\n"
-        "+++ bar	1969-12-31 19:00:00.000000000 -0500\n"
+        "--- bar\t2009-08-26 15:53:34.000000000 -0400\n"
+        "+++ bar\t1969-12-31 19:00:00.000000000 -0500\n"
         "@@ -1,3 +0,0 @@\n"
         "-a\n"
         "-b\n"
         "-c\n"
-        "--- baz	1969-12-31 19:00:00.000000000 -0500\n"
-        "+++ baz	2009-08-26 15:53:57.000000000 -0400\n"
+        "--- baz\t1969-12-31 19:00:00.000000000 -0500\n"
+        "+++ baz\t2009-08-26 15:53:57.000000000 -0400\n"
         "@@ -0,0 +1,2 @@\n"
         "+a\n"
         "+b\n"
-        "--- foo	2009-08-26 15:53:23.000000000 -0400\n"
-        "+++ foo	2009-08-26 15:56:43.000000000 -0400\n"
+        "--- foo\t2009-08-26 15:53:23.000000000 -0400\n"
+        "+++ foo\t2009-08-26 15:56:43.000000000 -0400\n"
         "@@ -1,3 +1,5 @@\n"
         " a\n"
         "-b\n"
@@ -467,7 +467,6 @@
         self.assertEqual('', diff.conflicts)
         self.assertFalse(diff.has_conflicts)
 
-
     def test_fromBranchMergeProposal(self):
         # Correctly generates a PreviewDiff from a BranchMergeProposal.
         bmp, source_rev_id, target_rev_id = self.createExampleMerge()

=== modified file 'lib/lp/code/tests/helpers.py'
--- lib/lp/code/tests/helpers.py	2010-08-31 00:16:41 +0000
+++ lib/lp/code/tests/helpers.py	2010-09-21 15:06:00 +0000
@@ -64,9 +64,14 @@
     """
     if date_created is None:
         date_created = revision_date
+    parent = branch.revision_history.last()
+    if parent is None:
+        parent_ids = []
+    else:
+        parent_ids = [parent.revision.revision_id]
     revision = factory.makeRevision(
         revision_date=revision_date, date_created=date_created,
-        log_body=commit_msg)
+        log_body=commit_msg, parent_ids=parent_ids)
     if mainline:
         sequence = branch.revision_count + 1
         branch_revision = branch.createBranchRevision(sequence, revision)
@@ -112,7 +117,7 @@
     preview.remvoed_lines_count = 13
     preview.diffstat = {'file1': (3, 8), 'file2': (4, 5)}
     return {
-        'eric': eric, 'fooix': fooix, 'trunk':trunk, 'feature': feature,
+        'eric': eric, 'fooix': fooix, 'trunk': trunk, 'feature': feature,
         'proposed': proposed, 'fred': fred}
 
 

=== modified file 'lib/lp/code/tests/test_directbranchcommit.py'
--- lib/lp/code/tests/test_directbranchcommit.py	2010-08-20 20:31:18 +0000
+++ lib/lp/code/tests/test_directbranchcommit.py	2010-09-21 15:06:00 +0000
@@ -99,6 +99,24 @@
         branch_revision_id = self.committer.bzrbranch.last_revision()
         self.assertEqual(branch_revision_id, revision_id)
 
+    def test_commit_uses_merge_parents(self):
+        # DirectBranchCommit.commit returns uses merge parents
+        self._tearDownCommitter()
+        # Merge parents cannot be specified for initial commit, so do an
+        # empty commit.
+        self.tree.commit('foo', committer='foo@bar', rev_id='foo')
+        committer = DirectBranchCommit(
+            self.db_branch, merge_parents=['parent-1', 'parent-2'])
+        committer.last_scanned_id = (
+            committer.bzrbranch.last_revision())
+        committer.writeFile('file.txt', 'contents')
+        revision_id = committer.commit('')
+        branch_revision_id = committer.bzrbranch.last_revision()
+        branch_revision = committer.bzrbranch.repository.get_revision(
+            branch_revision_id)
+        self.assertEqual(
+            ['parent-1', 'parent-2'], branch_revision.parent_ids[1:])
+
     def test_DirectBranchCommit_aborts_cleanly(self):
         # If a DirectBranchCommit is not committed, its changes do not
         # go into the branch.

=== modified file 'lib/lp/codehosting/bzrutils.py'
--- lib/lp/codehosting/bzrutils.py	2010-08-20 20:31:18 +0000
+++ lib/lp/codehosting/bzrutils.py	2010-09-21 15:06:00 +0000
@@ -18,11 +18,13 @@
     'identical_formats',
     'install_oops_handler',
     'is_branch_stackable',
+    'read_locked',
     'remove_exception_logging_hook',
     'safe_open',
     'UnsafeUrlSeen',
     ]
 
+from contextlib import contextmanager
 import os
 import sys
 import threading
@@ -363,3 +365,12 @@
         return branch.get_stacked_on_url()
     except (NotStacked, UnstackableBranchFormat):
         return None
+
+
+@contextmanager
+def read_locked(branch):
+    branch.lock_read()
+    try:
+        yield
+    finally:
+        branch.unlock()