Merge lp:~cjwatson/launchpad/git-mp-diffs into lp:launchpad
- git-mp-diffs
- Merge into devel
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 | ||||
Related bugs: |
|
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')) |