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

Proposed by Colin Watson
Status: Merged
Merged at revision: 17471
Proposed branch: lp:~cjwatson/launchpad/git-mp-diffs
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/git-mp-register
Diff against target: 746 lines (+314/-78)
8 files modified
database/schema/security.cfg (+3/-1)
lib/lp/app/browser/stringformatter.py (+4/-2)
lib/lp/app/browser/tests/test_stringformatter.py (+36/-2)
lib/lp/code/githosting.py (+32/-0)
lib/lp/code/model/branchmergeproposaljob.py (+36/-33)
lib/lp/code/model/diff.py (+88/-37)
lib/lp/code/model/tests/test_branchmergeproposaljobs.py (+20/-1)
lib/lp/code/model/tests/test_diff.py (+95/-2)
To merge this branch: bzr merge lp:~cjwatson/launchpad/git-mp-diffs
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+257768@code.launchpad.net

Commit message

Generate preview diffs for Git-based merge proposals.

Description of the change

Generate preview diffs for Git-based merge proposals.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)
Revision history for this message
Colin Watson (cjwatson) :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'database/schema/security.cfg'
2--- database/schema/security.cfg 2015-04-21 10:34:24 +0000
3+++ database/schema/security.cfg 2015-04-30 13:12:11 +0000
4@@ -1,4 +1,4 @@
5-# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
6+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8 #
9 # Possible permissions: SELECT, INSERT, UPDATE, EXECUTE
10@@ -1937,6 +1937,8 @@
11 public.distribution = SELECT
12 public.distroseries = SELECT
13 public.emailaddress = SELECT
14+public.gitref = SELECT
15+public.gitrepository = SELECT
16 public.incrementaldiff = SELECT, INSERT
17 public.job = SELECT, INSERT, UPDATE
18 public.karma = SELECT, INSERT
19
20=== modified file 'lib/lp/app/browser/stringformatter.py'
21--- lib/lp/app/browser/stringformatter.py 2013-12-12 05:06:11 +0000
22+++ lib/lp/app/browser/stringformatter.py 2015-04-30 13:12:11 +0000
23@@ -1,4 +1,4 @@
24-# Copyright 2010-2012 Canonical Ltd. This software is licensed under the
25+# Copyright 2010-2015 Canonical Ltd. This software is licensed under the
26 # GNU Affero General Public License version 3 (see the file LICENSE).
27
28 """TALES formatter for strings."""
29@@ -884,7 +884,9 @@
30 for row, line in enumerate(text.splitlines()[:max_format_lines]):
31 result.append('<tr id="diff-line-%s">' % (row + 1))
32 result.append('<td class="line-no">%s</td>' % (row + 1))
33- if line.startswith('==='):
34+ if (line.startswith('===') or
35+ line.startswith('diff') or
36+ line.startswith('index')):
37 css_class = 'diff-file text'
38 header_next = True
39 elif (header_next and
40
41=== modified file 'lib/lp/app/browser/tests/test_stringformatter.py'
42--- lib/lp/app/browser/tests/test_stringformatter.py 2013-12-13 02:09:14 +0000
43+++ lib/lp/app/browser/tests/test_stringformatter.py 2015-04-30 13:12:11 +0000
44@@ -1,4 +1,4 @@
45-# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
46+# Copyright 2010-2015 Canonical Ltd. This software is licensed under the
47 # GNU Affero General Public License version 3 (see the file LICENSE).
48
49 """Unit tests for the string TALES formatter."""
50@@ -165,7 +165,7 @@
51
52
53 class TestLinkifyingProtocols(TestCaseWithFactory):
54-
55+
56 layer = DatabaseFunctionalLayer
57
58 def test_normal_set(self):
59@@ -357,6 +357,40 @@
60 'diff-comment text'],
61 [str(tag['class']) for tag in text])
62
63+ def test_cssClasses_git(self):
64+ # Git diffs look slightly different, so check that they also end up
65+ # with the correct CSS classes.
66+ diff = dedent('''\
67+ diff --git a/tales.py b/tales.py
68+ index aaaaaaa..bbbbbbb 100644
69+ --- a/tales.py
70+ +++ b/tales.py
71+ @@ -2435,6 +2435,8 @@
72+ def format_diff(self):
73+ - removed this line
74+ + added this line
75+ -------- a sql style comment
76+ ++++++++ a line of pluses
77+ ''')
78+ html = FormattersAPI(diff).format_diff()
79+ line_numbers = find_tags_by_class(html, 'line-no')
80+ self.assertEqual(
81+ ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'],
82+ [tag.renderContents() for tag in line_numbers])
83+ text = find_tags_by_class(html, 'text')
84+ self.assertEqual(
85+ ['diff-file text',
86+ 'diff-file text',
87+ 'diff-header text',
88+ 'diff-header text',
89+ 'diff-chunk text',
90+ 'text',
91+ 'diff-removed text',
92+ 'diff-added text',
93+ 'diff-removed text',
94+ 'diff-added text'],
95+ [str(tag['class']) for tag in text])
96+
97 def test_config_value_limits_line_count(self):
98 # The config.diff.max_line_format contains the maximum number of lines
99 # to format.
100
101=== modified file 'lib/lp/code/githosting.py'
102--- lib/lp/code/githosting.py 2015-03-31 00:59:44 +0000
103+++ lib/lp/code/githosting.py 2015-04-30 13:12:11 +0000
104@@ -101,3 +101,35 @@
105 except ValueError as e:
106 raise GitRepositoryScanFault(
107 "Failed to decode commit-scan response: %s" % unicode(e))
108+
109+ def getMergeDiff(self, path, base, head, logger=None):
110+ """Get the merge preview diff between two commits.
111+
112+ :return: A dict mapping 'commits' to a list of commits between
113+ 'base' and 'head' (formatted as with `getCommits`), 'patch' to
114+ the text of the diff between 'base' and 'head', and 'conflicts'
115+ to a list of conflicted paths.
116+ """
117+ try:
118+ if logger is not None:
119+ logger.info(
120+ "Requesting merge diff for %s from %s to %s" % (
121+ path, base, head))
122+ response = self._makeSession().get(
123+ urlutils.join(
124+ self.endpoint, "repo", path, "compare-merge",
125+ "%s:%s" % (base, head)),
126+ timeout=self.timeout)
127+ except Exception as e:
128+ raise GitRepositoryScanFault(
129+ "Failed to get merge diff from Git repository: %s" %
130+ unicode(e))
131+ if response.status_code != 200:
132+ raise GitRepositoryScanFault(
133+ "Failed to get merge diff from Git repository: %s" %
134+ unicode(e))
135+ try:
136+ return response.json()
137+ except ValueError as e:
138+ raise GitRepositoryScanFault(
139+ "Failed to decode merge-diff response: %s" % unicode(e))
140
141=== modified file 'lib/lp/code/model/branchmergeproposaljob.py'
142--- lib/lp/code/model/branchmergeproposaljob.py 2014-05-28 12:23:10 +0000
143+++ lib/lp/code/model/branchmergeproposaljob.py 2015-04-30 13:12:11 +0000
144@@ -1,4 +1,4 @@
145-# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
146+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
147 # GNU Affero General Public License version 3 (see the file LICENSE).
148
149 """Job classes related to BranchMergeProposals are in here.
150@@ -21,6 +21,7 @@
151 'UpdatePreviewDiffJob',
152 ]
153
154+from contextlib import contextmanager
155 from datetime import (
156 datetime,
157 timedelta,
158@@ -39,7 +40,6 @@
159 Desc,
160 Or,
161 )
162-from storm.info import ClassAlias
163 from storm.locals import (
164 Int,
165 Reference,
166@@ -102,6 +102,7 @@
167 from lp.services.mail.sendmail import format_address_for_person
168 from lp.services.webapp import canonical_url
169
170+
171 class BranchMergeProposalJobType(DBEnumeratedType):
172 """Values that ICodeImportJob.state can take."""
173
174@@ -235,7 +236,7 @@
175 return '<%(job_type)s job for merge %(merge_id)s on %(branch)s>' % {
176 'job_type': self.context.job_type.name,
177 'merge_id': bmp.id,
178- 'branch': bmp.source_branch.unique_name,
179+ 'branch': bmp.merge_source.unique_name,
180 }
181
182 @classmethod
183@@ -278,12 +279,13 @@
184 Job.id.is_in(Job.ready_jobs),
185 BranchMergeProposalJob.branch_merge_proposal
186 == BranchMergeProposal.id,
187- BranchMergeProposal.source_branch == Branch.id,
188- # A proposal isn't considered ready if it has no revisions,
189- # or if it is hosted but pending a mirror.
190- Branch.revision_count > 0,
191- Or(Branch.next_mirror_time == None,
192- Branch.branch_type != BranchType.HOSTED)))
193+ Or(And(BranchMergeProposal.source_branch == Branch.id,
194+ # A proposal isn't considered ready if it has no
195+ # revisions, or if it is hosted but pending a mirror.
196+ Branch.revision_count > 0,
197+ Or(Branch.next_mirror_time == None,
198+ Branch.branch_type != BranchType.HOSTED)),
199+ BranchMergeProposal.source_git_repository != None)))
200 return (klass(job) for job in jobs)
201
202 def getOopsVars(self):
203@@ -293,8 +295,8 @@
204 vars.extend([
205 ('branchmergeproposal_job_id', self.context.id),
206 ('branchmergeproposal_job_type', self.context.job_type.title),
207- ('source_branch', bmp.source_branch.unique_name),
208- ('target_branch', bmp.target_branch.unique_name)])
209+ ('merge_source', bmp.merge_source.unique_name),
210+ ('merge_target', bmp.merge_target.unique_name)])
211 return vars
212
213
214@@ -320,8 +322,8 @@
215
216 def getOperationDescription(self):
217 return ('notifying people about the proposal to merge %s into %s' %
218- (self.branch_merge_proposal.source_branch.bzr_identity,
219- self.branch_merge_proposal.target_branch.bzr_identity))
220+ (self.branch_merge_proposal.merge_source.identity,
221+ self.branch_merge_proposal.merge_target.identity))
222
223
224 class UpdatePreviewDiffJob(BranchMergeProposalJobDerived):
225@@ -350,15 +352,16 @@
226 """Is this job ready to run?"""
227 bmp = self.branch_merge_proposal
228 url = canonical_url(bmp)
229- if bmp.source_branch.last_scanned_id is None:
230- raise UpdatePreviewDiffNotReady(
231- 'The source branch of %s has no revisions.' % url)
232- if bmp.target_branch.last_scanned_id is None:
233- raise UpdatePreviewDiffNotReady(
234- 'The target branch of %s has no revisions.' % url)
235- if bmp.source_branch.pending_writes:
236- raise BranchHasPendingWrites(
237- 'The source branch of %s has pending writes.' % url)
238+ if bmp.source_branch is not None:
239+ if bmp.source_branch.last_scanned_id is None:
240+ raise UpdatePreviewDiffNotReady(
241+ 'The source branch of %s has no revisions.' % url)
242+ if bmp.target_branch.last_scanned_id is None:
243+ raise UpdatePreviewDiffNotReady(
244+ 'The target branch of %s has no revisions.' % url)
245+ if bmp.source_branch.pending_writes:
246+ raise BranchHasPendingWrites(
247+ 'The source branch of %s has pending writes.' % url)
248
249 def acquireLease(self, duration=600):
250 return self.job.acquireLease(duration)
251@@ -366,7 +369,13 @@
252 def run(self):
253 """See `IRunnableJob`."""
254 self.checkReady()
255- with server(get_ro_server(), no_replace=True):
256+ if self.branch_merge_proposal.source_branch is not None:
257+ server_context = server(get_ro_server(), no_replace=True)
258+ else:
259+ # A no-op context manager. (This could be simplified with
260+ # contextlib.ExitStack from Python 3.3.)
261+ server_context = contextmanager(lambda: (None for _ in [None]))()
262+ with server_context:
263 with BranchMergeProposalDelta.monitor(self.branch_merge_proposal):
264 PreviewDiff.fromBranchMergeProposal(self.branch_merge_proposal)
265
266@@ -655,22 +664,16 @@
267
268 @staticmethod
269 def iterReady(job_type=None):
270- from lp.code.model.branch import Branch
271- SourceBranch = ClassAlias(Branch)
272- TargetBranch = ClassAlias(Branch)
273 clauses = [
274 BranchMergeProposalJob.job == Job.id,
275 Job._status.is_in([JobStatus.WAITING, JobStatus.RUNNING]),
276 BranchMergeProposalJob.branch_merge_proposal ==
277- BranchMergeProposal.id, BranchMergeProposal.source_branch ==
278- SourceBranch.id, BranchMergeProposal.target_branch ==
279- TargetBranch.id,
280+ BranchMergeProposal.id,
281 ]
282 if job_type is not None:
283 clauses.append(BranchMergeProposalJob.job_type == job_type)
284- jobs = IMasterStore(Branch).find(
285- (BranchMergeProposalJob, Job, BranchMergeProposal,
286- SourceBranch, TargetBranch), And(*clauses))
287+ jobs = IMasterStore(BranchMergeProposalJob).find(
288+ (BranchMergeProposalJob, Job, BranchMergeProposal), And(*clauses))
289 # Order by the job status first (to get running before waiting), then
290 # the date_created, then job type. This should give us all creation
291 # jobs before comment jobs.
292@@ -680,7 +683,7 @@
293 # Now only return one job for any given merge proposal.
294 ready_jobs = []
295 seen_merge_proposals = set()
296- for bmp_job, job, bmp, source, target in jobs:
297+ for bmp_job, job, bmp in jobs:
298 # If we've seen this merge proposal already, skip this job.
299 if bmp.id in seen_merge_proposals:
300 continue
301
302=== modified file 'lib/lp/code/model/diff.py'
303--- lib/lp/code/model/diff.py 2014-02-26 13:38:39 +0000
304+++ lib/lp/code/model/diff.py 2015-04-30 13:12:11 +0000
305@@ -1,4 +1,4 @@
306-# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
307+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
308 # GNU Affero General Public License version 3 (see the file LICENSE).
309
310 """Implementation classes for IDiff, etc."""
311@@ -41,6 +41,7 @@
312 from zope.interface import implements
313
314 from lp.app.errors import NotFoundError
315+from lp.code.githosting import GitHostingClient
316 from lp.code.interfaces.diff import (
317 IDiff,
318 IIncrementalDiff,
319@@ -233,7 +234,8 @@
320 return cls.fromFile(diff_content, size, filename)
321
322 @classmethod
323- def fromFile(cls, diff_content, size, filename=None):
324+ def fromFile(cls, diff_content, size, filename=None,
325+ strip_prefix_segments=0):
326 """Create a Diff from a textual diff.
327
328 :diff_content: The diff text
329@@ -254,7 +256,9 @@
330 diff_content_bytes = diff_content.read(size)
331 diff_lines_count = len(diff_content_bytes.strip().split('\n'))
332 try:
333- diffstat = cls.generateDiffstat(diff_content_bytes)
334+ diffstat = cls.generateDiffstat(
335+ diff_content_bytes,
336+ strip_prefix_segments=strip_prefix_segments)
337 except Exception:
338 getUtility(IErrorReportingUtility).raising(sys.exc_info())
339 # Set the diffstat to be empty.
340@@ -272,10 +276,13 @@
341 removed_lines_count=removed_lines_count)
342
343 @staticmethod
344- def generateDiffstat(diff_bytes):
345+ def generateDiffstat(diff_bytes, strip_prefix_segments=0):
346 """Generate statistics about the provided diff.
347
348 :param diff_bytes: A unified diff, as bytes.
349+ :param strip_prefix_segments: Strip the smallest prefix containing
350+ this many leading slashes from each file name found in the patch
351+ file, as with "patch -p".
352 :return: A map of {filename: (added_line_count, removed_line_count)}
353 """
354 file_stats = {}
355@@ -285,6 +292,8 @@
356 if not isinstance(patch, Patch):
357 continue
358 path = patch.newname.split('\t')[0]
359+ if strip_prefix_segments:
360+ path = path.split('/', strip_prefix_segments)[-1]
361 file_stats[path] = tuple(patch.stats_values()[:2])
362 return file_stats
363
364@@ -378,27 +387,39 @@
365 # diff was generated for absent branch revisions (e.g. the source
366 # or target branch was overwritten). Which means some entries for
367 # the same BMP may have much wider titles depending on the
368- # branch history. It is particulary bad for rendering the diff
369+ # branch history. It is particularly bad for rendering the diff
370 # navigator 'select' widget in the UI.
371- source_rev = bmp.source_branch.getBranchRevision(
372- revision_id=self.source_revision_id)
373- if source_rev and source_rev.sequence:
374- source_revno = u'r{0}'.format(source_rev.sequence)
375- else:
376- source_revno = self.source_revision_id
377- target_rev = bmp.target_branch.getBranchRevision(
378- revision_id=self.target_revision_id)
379- if target_rev and target_rev.sequence:
380- target_revno = u'r{0}'.format(target_rev.sequence)
381- else:
382- target_revno = self.target_revision_id
383+ if bmp.source_branch is not None:
384+ source_revision = bmp.source_branch.getBranchRevision(
385+ revision_id=self.source_revision_id)
386+ if source_revision and source_revision.sequence:
387+ source_rev = u'r{0}'.format(source_revision.sequence)
388+ else:
389+ source_rev = self.source_revision_id
390+ target_revision = bmp.target_branch.getBranchRevision(
391+ revision_id=self.target_revision_id)
392+ if target_revision and target_revision.sequence:
393+ target_rev = u'r{0}'.format(target_revision.sequence)
394+ else:
395+ target_rev = self.target_revision_id
396+ else:
397+ # For Git, we shorten to seven characters since that's usual.
398+ # We should perhaps shorten only as far as preserves uniqueness,
399+ # but that requires talking to the hosting service and it's
400+ # unlikely to be a problem in practice.
401+ source_rev = self.source_revision_id[:7]
402+ target_rev = self.target_revision_id[:7]
403
404- return u'{0} into {1}'.format(source_revno, target_revno)
405+ return u'{0} into {1}'.format(source_rev, target_rev)
406
407 @property
408 def has_conflicts(self):
409 return self.conflicts is not None and self.conflicts != ''
410
411+ @staticmethod
412+ def getGitHostingClient():
413+ return GitHostingClient(config.codehosting.internal_git_api_endpoint)
414+
415 @classmethod
416 def fromBranchMergeProposal(cls, bmp):
417 """Create a `PreviewDiff` from a `BranchMergeProposal`.
418@@ -407,34 +428,62 @@
419 :param bmp: The `BranchMergeProposal` to generate a `PreviewDiff` for.
420 :return: A `PreviewDiff`.
421 """
422- source_branch = bmp.source_branch.getBzrBranch()
423- source_revision = source_branch.last_revision()
424- target_branch = bmp.target_branch.getBzrBranch()
425- target_revision = target_branch.last_revision()
426- if bmp.prerequisite_branch is not None:
427- prerequisite_branch = bmp.prerequisite_branch.getBzrBranch()
428+ if bmp.source_branch is not None:
429+ source_branch = bmp.source_branch.getBzrBranch()
430+ source_revision = source_branch.last_revision()
431+ target_branch = bmp.target_branch.getBzrBranch()
432+ target_revision = target_branch.last_revision()
433+ if bmp.prerequisite_branch is not None:
434+ prerequisite_branch = bmp.prerequisite_branch.getBzrBranch()
435+ else:
436+ prerequisite_branch = None
437+ diff, conflicts = Diff.mergePreviewFromBranches(
438+ source_branch, source_revision, target_branch,
439+ prerequisite_branch)
440+ preview = cls()
441+ preview.source_revision_id = source_revision.decode('utf-8')
442+ preview.target_revision_id = target_revision.decode('utf-8')
443+ preview.branch_merge_proposal = bmp
444+ preview.diff = diff
445+ preview.conflicts = u''.join(
446+ unicode(conflict) + '\n' for conflict in conflicts)
447 else:
448- prerequisite_branch = None
449- diff, conflicts = Diff.mergePreviewFromBranches(
450- source_branch, source_revision, target_branch, prerequisite_branch)
451- preview = cls()
452- preview.source_revision_id = source_revision.decode('utf-8')
453- preview.target_revision_id = target_revision.decode('utf-8')
454- preview.branch_merge_proposal = bmp
455- preview.diff = diff
456- preview.conflicts = u''.join(
457- unicode(conflict) + '\n' for conflict in conflicts)
458+ hosting_client = cls.getGitHostingClient()
459+ source_repository = bmp.source_git_repository
460+ target_repository = bmp.target_git_repository
461+ if source_repository == target_repository:
462+ path = source_repository.getInternalPath()
463+ else:
464+ path = "%s:%s" % (
465+ target_repository.getInternalPath(),
466+ source_repository.getInternalPath())
467+ # XXX cjwatson 2015-04-30: Add prerequisite handling once turnip
468+ # supports it.
469+ response = hosting_client.getMergeDiff(
470+ path, bmp.source_git_ref.commit_sha1,
471+ bmp.target_git_ref.commit_sha1)
472+ source_revision_id = bmp.source_git_ref.commit_sha1
473+ target_revision_id = bmp.target_git_ref.commit_sha1
474+ if bmp.prerequisite_git_ref is not None:
475+ prerequisite_revision_id = bmp.prerequisite_git_ref.commit_sha1
476+ else:
477+ prerequisite_revision_id = None
478+ conflicts = u"".join(
479+ u"Conflict in %s\n" % path for path in response['conflicts'])
480+ preview = cls.create(
481+ bmp, response['patch'], source_revision_id, target_revision_id,
482+ prerequisite_revision_id, conflicts, strip_prefix_segments=1)
483 del get_property_cache(bmp).preview_diffs
484 del get_property_cache(bmp).preview_diff
485 return preview
486
487 @classmethod
488 def create(cls, bmp, diff_content, source_revision_id, target_revision_id,
489- prerequisite_revision_id, conflicts):
490+ prerequisite_revision_id, conflicts, strip_prefix_segments=0):
491 """Create a PreviewDiff with specified values.
492
493 :param bmp: The `BranchMergeProposal` this diff references.
494- :param diff_content: The text of the dift, as bytes.
495+ :param diff_content: The text of the diff, as bytes.
496 :param source_revision_id: The revision_id of the source branch.
497 :param target_revision_id: The revision_id of the target branch.
498 :param prerequisite_revision_id: The revision_id of the prerequisite
499@@ -444,7 +493,9 @@
500 """
501 filename = str(uuid1()) + '.txt'
502 size = len(diff_content)
503- diff = Diff.fromFile(StringIO(diff_content), size, filename)
504+ diff = Diff.fromFile(
505+ StringIO(diff_content), size, filename,
506+ strip_prefix_segments=strip_prefix_segments)
507
508 preview = cls()
509 preview.branch_merge_proposal = bmp
510
511=== modified file 'lib/lp/code/model/tests/test_branchmergeproposaljobs.py'
512--- lib/lp/code/model/tests/test_branchmergeproposaljobs.py 2014-05-29 07:08:17 +0000
513+++ lib/lp/code/model/tests/test_branchmergeproposaljobs.py 2015-04-30 13:12:11 +0000
514@@ -1,4 +1,4 @@
515-# Copyright 2010-2012 Canonical Ltd. This software is licensed under the
516+# Copyright 2010-2015 Canonical Ltd. This software is licensed under the
517 # GNU Affero General Public License version 3 (see the file LICENSE).
518
519 """Tests for branch merge proposal jobs."""
520@@ -38,6 +38,7 @@
521 IUpdatePreviewDiffJob,
522 IUpdatePreviewDiffJobSource,
523 )
524+from lp.code.interfaces.gitrepository import GIT_FEATURE_FLAG
525 from lp.code.model.branchmergeproposaljob import (
526 BranchMergeProposalJob,
527 BranchMergeProposalJobDerived,
528@@ -219,6 +220,14 @@
529 JobRunner([job]).runAll()
530 self.checkExampleMerge(bmp.preview_diff.text)
531
532+ def test_run_git(self):
533+ self.useFixture(FeatureFixture({GIT_FEATURE_FLAG: u"on"}))
534+ bmp, _, _, patch = self.createExampleGitMerge()
535+ job = UpdatePreviewDiffJob.create(bmp)
536+ with dbuser("merge-proposal-jobs"):
537+ JobRunner([job]).runAll()
538+ self.assertEqual(patch, bmp.preview_diff.text)
539+
540 def test_run_object_events(self):
541 # While the job runs a single IObjectModifiedEvent is issued when the
542 # preview diff has been calculated.
543@@ -514,6 +523,16 @@
544 self.assertEqual(job.branch_merge_proposal, bmp)
545 self.assertIsInstance(job, MergeProposalUpdatedEmailJob)
546
547+ def test_iterReady_supports_git(self):
548+ # iterReady supports merge proposals based on Git. (These are
549+ # currently considered ready regardless of scanning, since the hard
550+ # work is done by the backend.)
551+ self.useFixture(FeatureFixture({GIT_FEATURE_FLAG: u"on"}))
552+ bmp = self.factory.makeBranchMergeProposalForGit()
553+ [job] = self.job_source.iterReady()
554+ self.assertEqual(bmp, job.branch_merge_proposal)
555+ self.assertIsInstance(job, UpdatePreviewDiffJob)
556+
557
558 class TestCodeReviewCommentEmailJob(TestCaseWithFactory):
559
560
561=== modified file 'lib/lp/code/model/tests/test_diff.py'
562--- lib/lp/code/model/tests/test_diff.py 2014-02-26 13:38:39 +0000
563+++ lib/lp/code/model/tests/test_diff.py 2015-04-30 13:12:11 +0000
564@@ -1,4 +1,4 @@
565-# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
566+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
567 # GNU Affero General Public License version 3 (see the file LICENSE).
568
569 """Tests for Diff, etc."""
570@@ -8,6 +8,7 @@
571 from cStringIO import StringIO
572 from difflib import unified_diff
573 import logging
574+from textwrap import dedent
575
576 from bzrlib import trace
577 from bzrlib.patches import (
578@@ -15,6 +16,7 @@
579 parse_patches,
580 RemoveLine,
581 )
582+from fixtures import MonkeyPatch
583 import transaction
584 from zope.security.proxy import removeSecurityProxy
585
586@@ -24,11 +26,13 @@
587 IIncrementalDiff,
588 IPreviewDiff,
589 )
590+from lp.code.interfaces.gitrepository import GIT_FEATURE_FLAG
591 from lp.code.model.diff import (
592 Diff,
593 PreviewDiff,
594 )
595 from lp.code.model.directbranchcommit import DirectBranchCommit
596+from lp.services.features.testing import FeatureFixture
597 from lp.services.librarian.interfaces.client import (
598 LIBRARIAN_SERVER_DEFAULT_TIMEOUT,
599 )
600@@ -40,6 +44,7 @@
601 TestCaseWithFactory,
602 verifyObject,
603 )
604+from lp.testing.fakemethod import FakeMethod
605 from lp.testing.layers import (
606 LaunchpadFunctionalLayer,
607 LaunchpadZopelessLayer,
608@@ -86,6 +91,10 @@
609 return bmp, source_rev_id, target_rev_id
610
611
612+class FakeGitHostingClient:
613+ pass
614+
615+
616 class DiffTestCase(TestCaseWithFactory):
617
618 def createExampleMerge(self):
619@@ -128,6 +137,44 @@
620 return (source_bzr, source_rev_id, target_bzr, prerequisite_bzr,
621 prerequisite)
622
623+ def installFakeGitMergeDiff(self, result=None, failure=None):
624+ hosting_client = FakeGitHostingClient()
625+ hosting_client.getMergeDiff = FakeMethod(
626+ result=result, failure=failure)
627+ self.useFixture(MonkeyPatch(
628+ "lp.code.model.diff.PreviewDiff.getGitHostingClient",
629+ staticmethod(lambda: hosting_client)))
630+
631+ def createExampleGitMerge(self):
632+ """Create an example Git-based merge scenario.
633+
634+ The actual diff work is done in turnip, and we have no turnip
635+ fixture as yet, so for the moment we just install a fake hosting
636+ client that will return a plausible-looking diff.
637+ """
638+ bmp = self.factory.makeBranchMergeProposalForGit()
639+ source_sha1 = bmp.source_git_ref.commit_sha1
640+ target_sha1 = bmp.target_git_ref.commit_sha1
641+ patch = dedent("""\
642+ diff --git a/foo b/foo
643+ index %s..%s 100644
644+ --- a/foo
645+ +++ b/foo
646+ @@ -1,2 +1,7 @@
647+ +<<<<<<< foo
648+ c
649+ +=======
650+ +d
651+ +>>>>>>> foo
652+ a
653+ +b
654+ """) % (target_sha1[:7], source_sha1[:7])
655+ self.installFakeGitMergeDiff(result={
656+ "patch": patch,
657+ "conflicts": ["foo"],
658+ })
659+ return bmp, source_sha1, target_sha1, patch
660+
661
662 class TestDiff(DiffTestCase):
663
664@@ -329,6 +376,10 @@
665
666 layer = LaunchpadFunctionalLayer
667
668+ def setUp(self):
669+ super(TestPreviewDiff, self).setUp()
670+ self.useFixture(FeatureFixture({GIT_FEATURE_FLAG: u"on"}))
671+
672 def _createProposalWithPreviewDiff(self, prerequisite_branch=None,
673 content=None):
674 # Create and return a preview diff.
675@@ -348,6 +399,24 @@
676 transaction.commit()
677 return mp
678
679+ def _createGitProposalWithPreviewDiff(self, merge_prerequisite=None,
680+ content=None):
681+ mp = self.factory.makeBranchMergeProposalForGit(
682+ prerequisite_ref=merge_prerequisite)
683+ login_person(mp.registrant)
684+ if merge_prerequisite is None:
685+ prerequisite_revision_id = None
686+ else:
687+ prerequisite_revision_id = u"c" * 40
688+ if content is None:
689+ content = "".join(unified_diff("", "content"))
690+ mp.updatePreviewDiff(
691+ content, u"a" * 40, u"b" * 40,
692+ prerequisite_revision_id=prerequisite_revision_id)
693+ # Make sure the librarian file is written.
694+ transaction.commit()
695+ return mp
696+
697 def test_providesInterface(self):
698 # In order to test the interface provision, we need to make sure that
699 # the associated diff object that is delegated to is also created.
700@@ -365,7 +434,7 @@
701
702 def test_title(self):
703 # PreviewDiff has title and it copes with absent
704- # BranchRevision{.sequence} when branches get overridden/rebased.
705+ # BranchRevision{.sequence} when branches get overridden/rebased.
706 mp = self._createProposalWithPreviewDiff(content='')
707 preview = mp.preview_diff
708 # Both, source and target, branch revisions are absent,
709@@ -386,6 +455,12 @@
710 target_rev.sequence = 1
711 self.assertEqual('r10 into r1', preview.title)
712
713+ def test_title_git(self):
714+ # Git commit IDs are shortened to seven characters.
715+ mp = self._createGitProposalWithPreviewDiff(content='')
716+ preview = mp.preview_diff
717+ self.assertEqual('aaaaaaa into bbbbbbb', preview.title)
718+
719 def test_empty_diff(self):
720 # Once the source is merged into the target, the diff between the
721 # branches will be empty.
722@@ -495,6 +570,24 @@
723 finally:
724 logger.removeHandler(handler)
725
726+ def test_fromBranchMergeProposalForGit(self):
727+ # Correctly generates a PreviewDiff from a Git-based
728+ # BranchMergeProposal.
729+ bmp, source_rev_id, target_rev_id, patch = self.createExampleGitMerge()
730+ preview = PreviewDiff.fromBranchMergeProposal(bmp)
731+ self.assertEqual(source_rev_id, preview.source_revision_id)
732+ self.assertEqual(target_rev_id, preview.target_revision_id)
733+ transaction.commit()
734+ self.assertEqual(patch, preview.text)
735+ self.assertEqual({'foo': (5, 0)}, preview.diffstat)
736+
737+ def test_fromBranchMergeProposalForGit_sets_conflicts(self):
738+ # Conflicts are set on the PreviewDiff.
739+ bmp, source_rev_id, target_rev_id, _ = self.createExampleGitMerge()
740+ preview = PreviewDiff.fromBranchMergeProposal(bmp)
741+ self.assertEqual('Conflict in foo\n', preview.conflicts)
742+ self.assertTrue(preview.has_conflicts)
743+
744 def test_getFileByName(self):
745 diff = self._createProposalWithPreviewDiff().preview_diff
746 self.assertEqual(diff.diff_text, diff.getFileByName('preview.diff'))