Merge lp:~abentley/launchpad/resubmit-change-branch into lp:launchpad

Proposed by Aaron Bentley
Status: Merged
Approved by: Aaron Bentley
Approved revision: no longer in the source branch.
Merged at revision: 11904
Proposed branch: lp:~abentley/launchpad/resubmit-change-branch
Merge into: lp:launchpad
Diff against target: 407 lines (+218/-31)
6 files modified
lib/lp/code/browser/branchmergeproposal.py (+37/-5)
lib/lp/code/browser/tests/test_branchmergeproposal.py (+94/-1)
lib/lp/code/interfaces/branchmergeproposal.py (+25/-11)
lib/lp/code/model/branchmergeproposal.py (+20/-8)
lib/lp/code/model/tests/test_branchmergeproposal.py (+38/-0)
lib/lp/code/templates/branchmergeproposal-resubmit.pt (+4/-6)
To merge this branch: bzr merge lp:~abentley/launchpad/resubmit-change-branch
Reviewer Review Type Date Requested Status
Paul Hummer (community) ui Approve
Brad Crittenden (community) code Approve
Review via email: mp+40126@code.launchpad.net

Commit message

Allow changing branches, description when resubmitting merge proposal.

Description of the change

= Summary =
Fix bug #504369: Resubmit should allow changing the branches
Screenshot: http://people.canonical.com/~abentley/resubmit-change-branch.png

== Proposed fix ==
Add fields for the branches to the resubmit page. Also allow changing the
description. Also allow the user to break the link between the old and new
proposals.

== Pre-implementation notes ==
None

== Implementation details ==
Changed the schema to ResubmitSchema to add the break_link boolean.
Changed from MergeProposalEditView to LaunchpadFormView to avoid attempting to
adapt BranchMergeProposal to ResubmitSchema.
Changed the text to reflect the fact that users now control the branches.
Changed the text to refer to the current merge proposal as "this proposal",
since the new one doesn't exist yet.

== Tests ==
bin/test -vt proposal code

== Demo and Q/A ==
Create a merge proposal. Resubmit it, changing all branches, description, and
selecting "break link". A new proposal should be created with the specified
branches and description, and with no link to the previous one. The previous
one should be marked "superseded".

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/code/templates/branchmergeproposal-resubmit.pt
  lib/lp/code/interfaces/branchmergeproposal.py
  lib/lp/code/model/branchmergeproposal.py
  lib/lp/code/model/tests/test_branchmergeproposal.py
  lib/lp/code/browser/tests/test_branchmergeproposal.py
  lib/lp/code/browser/branchmergeproposal.py

./lib/lp/code/browser/branchmergeproposal.py
     986: E202 whitespace before ']'
     988: E301 expected 1 blank line, found 0

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

Thanks for the very nice cover letter Aaron.

The lint issues both look real and need to be fixed. The list for field_names should be multi-line with one entry per line ending with a trailing comma.

In 'initial_values' the list 'values' is really key, value pairs so calling it 'items' would make more sense.

In 'test_resubmit_text' your use of string concatenation the call to 'assertTextMatches...' is hard to read. Using a variable like
  expected = '...'
would be more readable.

s/If this is the same as the target branch/
  If this branch is the same as the target branch

Please provide a 'cancel_url' so that you get a cancel link.

Finally, it is clear you thought it out since you test for it explicitly, but I question the need to create a new merge proposal when nothing has changed. Do you have a good use case for that?

This new functionality is a vast improvement. Thanks for recognizing the need and providing the solution.

review: Approve (code)
Revision history for this message
Aaron Bentley (abentley) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

On 11/05/2010 01:42 AM, Brad Crittenden wrote:
> Finally, it is clear you thought it out since you test for it explicitly, but I question the need to create a new merge proposal when nothing has changed. Do you have a good use case for that?

Actually, that was the existing functionality. It is for cases where
the proposal is marked "resubmit", many changes are made, and finally
the proposal is resubmitted.

Aaron
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.10 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAkzUJpAACgkQ0F+nu1YWqI1WDwCdEKhJpAiuZXSzxf6/PK5gmzGO
wUUAnjmKdw60t5erpfaDAegkFkU22zuN
=houq
-----END PGP SIGNATURE-----

Revision history for this message
Paul Hummer (rockstar) wrote :

As discussed on the phone, I'm of the opinion that we should ensure that both the source and the target aren't changed. In cases like that, I think that we should encouraging people to create a whole new merge proposal. I've been outvoted in this case, but I thought I'd mention it in the MP just for history's sake.

review: Approve (ui)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/browser/branchmergeproposal.py'
2--- lib/lp/code/browser/branchmergeproposal.py 2010-09-29 07:09:03 +0000
3+++ lib/lp/code/browser/branchmergeproposal.py 2010-11-11 02:54:49 +0000
4@@ -57,6 +57,7 @@
5 Interface,
6 )
7 from zope.schema import (
8+ Bool,
9 Choice,
10 Int,
11 Text,
12@@ -965,20 +966,51 @@
13 % revision_name)
14
15
16-class BranchMergeProposalResubmitView(MergeProposalEditView,
17+class ResubmitSchema(IBranchMergeProposal):
18+
19+ break_link = Bool(
20+ title=u'Start afresh',
21+ description=(
22+ u'Do not show old conversation and do not link to superseded'
23+ ' proposal.'),
24+ default=False,)
25+
26+
27+class BranchMergeProposalResubmitView(LaunchpadFormView,
28 UnmergedRevisionsMixin):
29 """The view to resubmit a proposal to merge."""
30
31- schema = IBranchMergeProposal
32+ schema = ResubmitSchema
33+ for_input = True
34 page_title = label = "Resubmit proposal to merge"
35- field_names = []
36+ field_names = [
37+ 'source_branch',
38+ 'target_branch',
39+ 'prerequisite_branch',
40+ 'description',
41+ 'break_link',
42+ ]
43+
44+ def initialize(self):
45+ self.cancel_url = canonical_url(self.context)
46+ super(BranchMergeProposalResubmitView, self).initialize()
47+
48+ @property
49+ def initial_values(self):
50+ UNSET = object()
51+ items = ((key, getattr(self.context, key, UNSET)) for key in
52+ self.field_names if key != 'break_link')
53+ return dict(item for item in items if item[1] is not UNSET)
54
55 @action('Resubmit', name='resubmit')
56- @update_and_notify
57 def resubmit_action(self, action, data):
58 """Resubmit this proposal."""
59- proposal = self.context.resubmit(self.user)
60+ proposal = self.context.resubmit(
61+ self.user, data['source_branch'], data['target_branch'],
62+ data['prerequisite_branch'], data['description'],
63+ data['break_link'])
64 self.next_url = canonical_url(proposal)
65+ return proposal
66
67
68 class BranchMergeProposalEditView(MergeProposalEditView):
69
70=== modified file 'lib/lp/code/browser/tests/test_branchmergeproposal.py'
71--- lib/lp/code/browser/tests/test_branchmergeproposal.py 2010-10-25 05:33:20 +0000
72+++ lib/lp/code/browser/tests/test_branchmergeproposal.py 2010-11-11 02:54:49 +0000
73@@ -34,6 +34,7 @@
74 BranchMergeProposalChangeStatusView,
75 BranchMergeProposalContextMenu,
76 BranchMergeProposalMergedView,
77+ BranchMergeProposalResubmitView,
78 BranchMergeProposalVoteView,
79 DecoratedCodeReviewVoteReference,
80 ICodeReviewNewRevisions,
81@@ -53,7 +54,9 @@
82 make_merge_proposal_without_reviewers,
83 )
84 from lp.testing import (
85+ BrowserTestCase,
86 login_person,
87+ person_logged_in,
88 TestCaseWithFactory,
89 time_counter,
90 )
91@@ -501,7 +504,7 @@
92 self.assertOnePendingReview(proposal, reviewer)
93 self.assertIs(None, proposal.description)
94
95- def test_register_request_review_type(self):
96+ def test_register_request_review_type_branch_reviewer(self):
97 # We can ask for a specific review type. The target branch has a
98 # reviewer so that reviewer should be used.
99 target_branch, reviewer = self._makeTargetBranchWithReviewer()
100@@ -515,6 +518,96 @@
101 self.assertIs(None, proposal.description)
102
103
104+class TestBranchMergeProposalResubmitView(TestCaseWithFactory):
105+ """Test BranchMergeProposalResubmitView."""
106+
107+ layer = DatabaseFunctionalLayer
108+
109+ def createView(self):
110+ """Create the required view."""
111+ context = self.factory.makeBranchMergeProposal()
112+ self.useContext(person_logged_in(context.registrant))
113+ view = BranchMergeProposalResubmitView(
114+ context, LaunchpadTestRequest())
115+ view.initialize()
116+ return view
117+
118+ def test_resubmit_action(self):
119+ """resubmit_action resubmits the proposal."""
120+ view = self.createView()
121+ context = view.context
122+ new_proposal = view.resubmit_action.success(
123+ {'source_branch': context.source_branch,
124+ 'target_branch': context.target_branch,
125+ 'prerequisite_branch': context.prerequisite_branch,
126+ 'description': None,
127+ 'break_link': False,
128+ })
129+ self.assertEqual(new_proposal.supersedes, context)
130+ self.assertEqual(new_proposal.source_branch, context.source_branch)
131+ self.assertEqual(new_proposal.target_branch, context.target_branch)
132+ self.assertEqual(
133+ new_proposal.prerequisite_branch, context.prerequisite_branch)
134+
135+ def test_resubmit_action_change_branches(self):
136+ """Changing the branches changes the branches in the new proposal."""
137+ view = self.createView()
138+ target = view.context.source_branch.target
139+ new_source = self.factory.makeBranchTargetBranch(target)
140+ new_target = self.factory.makeBranchTargetBranch(target)
141+ new_prerequisite = self.factory.makeBranchTargetBranch(target)
142+ new_proposal = view.resubmit_action.success(
143+ {'source_branch': new_source, 'target_branch': new_target,
144+ 'prerequisite_branch': new_prerequisite,
145+ 'description': 'description',
146+ 'break_link': False,
147+ })
148+ self.assertEqual(new_proposal.supersedes, view.context)
149+ self.assertEqual(new_proposal.source_branch, new_source)
150+ self.assertEqual(new_proposal.target_branch, new_target)
151+ self.assertEqual(new_proposal.prerequisite_branch, new_prerequisite)
152+
153+ def test_resubmit_action_break_link(self):
154+ """Enabling break_link prevents linking the old and new proposals."""
155+ view = self.createView()
156+ context = view.context
157+ new_proposal = view.resubmit_action.success(
158+ {'source_branch': context.source_branch,
159+ 'target_branch': context.target_branch,
160+ 'prerequisite_branch': context.prerequisite_branch,
161+ 'description': None,
162+ 'break_link': True,
163+ })
164+ self.assertIs(None, new_proposal.supersedes)
165+
166+
167+class TestResubmitBrowser(BrowserTestCase):
168+ """Browser tests for resubmitting branch merge proposals."""
169+
170+ layer = DatabaseFunctionalLayer
171+
172+ def test_resubmit_text(self):
173+ """The text of the resubmit page is as expected."""
174+ bmp = self.factory.makeBranchMergeProposal(registrant=self.user)
175+ text = self.getMainText(bmp, '+resubmit')
176+ expected = (
177+ 'Resubmit proposal to merge.*'
178+ 'Source Branch:.*'
179+ 'Target Branch:.*'
180+ 'Prerequisite Branch:.*'
181+ 'Description.*'
182+ 'Start afresh.*')
183+ self.assertTextMatchesExpressionIgnoreWhitespace(expected, text)
184+
185+ def test_resubmit_controls(self):
186+ """Proposals can be resubmitted using the browser."""
187+ bmp = self.factory.makeBranchMergeProposal(registrant=self.user)
188+ browser = self.getViewBrowser(bmp, '+resubmit')
189+ browser.getControl('Description').value = 'flibble'
190+ browser.getControl('Resubmit').click()
191+ self.assertEqual('flibble', bmp.superseded_by.description)
192+
193+
194 class TestBranchMergeProposalView(TestCaseWithFactory):
195
196 layer = LaunchpadFunctionalLayer
197
198=== modified file 'lib/lp/code/interfaces/branchmergeproposal.py'
199--- lib/lp/code/interfaces/branchmergeproposal.py 2010-10-25 05:33:20 +0000
200+++ lib/lp/code/interfaces/branchmergeproposal.py 2010-11-11 02:54:49 +0000
201@@ -45,6 +45,7 @@
202 from lazr.restful.fields import (
203 CollectionField,
204 Reference,
205+ ReferenceChoice,
206 )
207 from zope.event import notify
208 from zope.interface import (
209@@ -62,6 +63,7 @@
210 TextLine,
211 )
212
213+from canonical.database.constants import DEFAULT
214 from canonical.launchpad import _
215 from canonical.launchpad.interfaces.launchpad import IPrivacy
216 from canonical.launchpad.webapp.interfaces import ITableBatchNavigator
217@@ -112,25 +114,26 @@
218 description=_('The person who registered the landing target.')))
219
220 source_branch = exported(
221- Reference(
222- title=_('Source Branch'), schema=IBranch,
223+ ReferenceChoice(
224+ title=_('Source Branch'), schema=IBranch, vocabulary='Branch',
225 required=True, readonly=True,
226 description=_("The branch that has code to land.")))
227
228 target_branch = exported(
229- Reference(
230+ ReferenceChoice(
231 title=_('Target Branch'),
232- schema=IBranch, required=True, readonly=True,
233+ schema=IBranch, vocabulary='Branch', required=True, readonly=True,
234 description=_(
235 "The branch that the source branch will be merged into.")))
236
237 prerequisite_branch = exported(
238- Reference(
239- title=_('Dependent Branch'),
240- schema=IBranch, required=False, readonly=True,
241- description=_("The branch that the source branch branched from. "
242- "If this is the same as the target branch, then "
243- "leave this field blank.")))
244+ ReferenceChoice(
245+ title=_('Prerequisite Branch'),
246+ schema=IBranch, vocabulary='Branch', required=False,
247+ readonly=True, description=_(
248+ "The branch that the source branch branched from. "
249+ "If this branch is the same as the target branch, then "
250+ "leave this field blank.")))
251
252 # This is redefined from IPrivacy.private because the attribute is
253 # read-only. The value is determined by the involved branches.
254@@ -430,13 +433,24 @@
255 :type merge_reporter: ``Person``
256 """
257
258- def resubmit(registrant):
259+ def resubmit(registrant, source_branch=None, target_branch=None,
260+ prerequisite_branch=DEFAULT):
261 """Mark the branch merge proposal as superseded and return a new one.
262
263 The new proposal is created as work-in-progress, and copies across
264 user-entered data like the whiteboard. All the current proposal's
265 reviewers, including those who have only been nominated, are requested
266 to review the new proposal.
267+
268+ :param registrant: The person registering the new proposal.
269+ :param source_branch: The source_branch for the new proposal (defaults
270+ to the current source_branch).
271+ :param target_branch: The target_branch for the new proposal (defaults
272+ to the current target_branch).
273+ :param prerequisite_branch: The prerequisite_branch for the new
274+ proposal (defaults to the current prerequisite_branch).
275+ :param description: The description for the new proposal (defaults to
276+ the current description).
277 """
278
279 def isMergable():
280
281=== modified file 'lib/lp/code/model/branchmergeproposal.py'
282--- lib/lp/code/model/branchmergeproposal.py 2010-10-19 00:44:24 +0000
283+++ lib/lp/code/model/branchmergeproposal.py 2010-11-11 02:54:49 +0000
284@@ -543,8 +543,19 @@
285 date_merged = UTC_NOW
286 self.date_merged = date_merged
287
288- def resubmit(self, registrant):
289+ def resubmit(self, registrant, source_branch=None, target_branch=None,
290+ prerequisite_branch=DEFAULT, description=None,
291+ break_link=False):
292 """See `IBranchMergeProposal`."""
293+ if source_branch is None:
294+ source_branch = self.source_branch
295+ if target_branch is None:
296+ target_branch = self.target_branch
297+ # DEFAULT instead of None, because None is a valid value.
298+ if prerequisite_branch is DEFAULT:
299+ prerequisite_branch = self.prerequisite_branch
300+ if description is None:
301+ description = self.description
302 # You can transition from REJECTED to SUPERSEDED, but
303 # not from MERGED or SUPERSEDED.
304 self._transitionToState(
305@@ -553,15 +564,16 @@
306 # a database query to identify if there are any active proposals
307 # with the same source and target branches.
308 self.syncUpdate()
309- review_requests = set(
310- (vote.reviewer, vote.review_type) for vote in self.votes)
311- proposal = self.source_branch.addLandingTarget(
312+ review_requests = list(set(
313+ (vote.reviewer, vote.review_type) for vote in self.votes))
314+ proposal = source_branch.addLandingTarget(
315 registrant=registrant,
316- target_branch=self.target_branch,
317- prerequisite_branch=self.prerequisite_branch,
318- description=self.description,
319+ target_branch=target_branch,
320+ prerequisite_branch=prerequisite_branch,
321+ description=description,
322 needs_review=True, review_requests=review_requests)
323- self.superseded_by = proposal
324+ if not break_link:
325+ self.superseded_by = proposal
326 # This sync update is needed to ensure that the transitive
327 # properties of supersedes and superseded_by are visible to
328 # the old and the new proposal.
329
330=== modified file 'lib/lp/code/model/tests/test_branchmergeproposal.py'
331--- lib/lp/code/model/tests/test_branchmergeproposal.py 2010-11-03 04:27:13 +0000
332+++ lib/lp/code/model/tests/test_branchmergeproposal.py 2010-11-11 02:54:49 +0000
333@@ -1659,6 +1659,9 @@
334 bmp2.queue_status, BranchMergeProposalStatus.NEEDS_REVIEW)
335 self.assertEqual(
336 bmp2, bmp1.superseded_by)
337+ self.assertEqual(bmp1.source_branch, bmp2.source_branch)
338+ self.assertEqual(bmp1.target_branch, bmp2.target_branch)
339+ self.assertEqual(bmp1.prerequisite_branch, bmp2.prerequisite_branch)
340
341 def test_resubmit_re_requests_review(self):
342 """Resubmit should request new reviews.
343@@ -1680,6 +1683,41 @@
344 (reviewer, 'specious')]),
345 set((vote.reviewer, vote.review_type) for vote in bmp2.votes))
346
347+ def test_resubmit_no_reviewers(self):
348+ """Resubmitting a proposal with no reviewers should work."""
349+ bmp = make_merge_proposal_without_reviewers(self.factory)
350+ with person_logged_in(bmp.registrant):
351+ bmp2 = bmp.resubmit(bmp.registrant)
352+
353+ def test_resubmit_changes_branches(self):
354+ """Resubmit changes branches, if specified."""
355+ original = self.factory.makeBranchMergeProposal()
356+ self.useContext(person_logged_in(original.registrant))
357+ branch_target = original.source_branch.target
358+ new_source = self.factory.makeBranchTargetBranch(branch_target)
359+ new_target = self.factory.makeBranchTargetBranch(branch_target)
360+ new_prerequisite = self.factory.makeBranchTargetBranch(branch_target)
361+ revised = original.resubmit(original.registrant, new_source,
362+ new_target, new_prerequisite)
363+ self.assertEqual(new_source, revised.source_branch)
364+ self.assertEqual(new_target, revised.target_branch)
365+ self.assertEqual(new_prerequisite, revised.prerequisite_branch)
366+
367+ def test_resubmit_changes_description(self):
368+ """Resubmit changes description, if specified."""
369+ original = self.factory.makeBranchMergeProposal()
370+ self.useContext(person_logged_in(original.registrant))
371+ revised = original.resubmit(original.registrant, description='foo')
372+ self.assertEqual('foo', revised.description)
373+
374+ def test_resubmit_breaks_link(self):
375+ """Resubmit breaks link, if specified."""
376+ original = self.factory.makeBranchMergeProposal()
377+ self.useContext(person_logged_in(original.registrant))
378+ revised = original.resubmit(
379+ original.registrant, break_link=True)
380+ self.assertIs(None, original.superseded_by)
381+
382
383 class TestCreateMergeProposalJob(TestCaseWithFactory):
384 """Tests for CreateMergeProposalJob."""
385
386=== modified file 'lib/lp/code/templates/branchmergeproposal-resubmit.pt'
387--- lib/lp/code/templates/branchmergeproposal-resubmit.pt 2009-10-29 13:58:43 +0000
388+++ lib/lp/code/templates/branchmergeproposal-resubmit.pt 2010-11-11 02:54:49 +0000
389@@ -14,14 +14,12 @@
390 <div metal:fill-slot="extra_info">
391 <p>
392 Resubmitting this proposal to merge will cause this proposal to be
393- marked as <strong>superseded</strong>. Another merge proposal will
394- be created, with the same source, target and prerequisite branch
395- (if any).
396+ marked as <strong>superseded</strong>. A new proposal will
397+ be created.
398 </p>
399 <p>
400- Everyone who has reviewed the previous proposal or was requested to
401- review the previous proposal will be requested to review the new
402- proposal.
403+ Everyone who has reviewed this proposal or was requested to review this
404+ proposal will be requested to review the new proposal.
405 </p>
406 </div>
407 </div>