launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #18418
[Merge] lp:~cjwatson/launchpad/git-mp-diffs into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/git-mp-diffs into lp:launchpad with lp:~cjwatson/launchpad/git-mp-register as a prerequisite.
Commit message:
Generate preview diffs for Git-based merge proposals.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1445017 in Launchpad itself: "Support for Launchpad Git merge proposals "
https://bugs.launchpad.net/launchpad/+bug/1445017
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/git-mp-diffs/+merge/257768
Generate preview diffs for Git-based merge proposals.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/git-mp-diffs into lp:launchpad.
=== 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)])
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
# 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):
"""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)
+ 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.)
+ 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'))
Follow ups