Merge lp:~dpb/tarmac/better-prereq-branches into lp:tarmac

Proposed by David Britton
Status: Merged
Approved by: dobey
Approved revision: 428
Merged at revision: 426
Proposed branch: lp:~dpb/tarmac/better-prereq-branches
Merge into: lp:tarmac
Diff against target: 347 lines (+162/-23)
5 files modified
.bzrignore (+1/-0)
docs/introduction.txt (+15/-1)
tarmac/bin/commands.py (+43/-13)
tarmac/tests/__init__.py (+1/-0)
tarmac/tests/test_commands.py (+102/-9)
To merge this branch: bzr merge lp:~dpb/tarmac/better-prereq-branches
Reviewer Review Type Date Requested Status
dobey Approve
Review via email: mp+197969@code.launchpad.net

Commit message

Better prerequisite handling.

Description of the change

Tarmac understands the launchpad concept of prerequisite branches. The
following conditions are supported when evaluating a candidate merge
proposal (MP).

 * Multiple prerequisite non-superseded MPs => error
 * Prerequiste branch but no associated MP => error
 * Exactly one prerequisite non-superseded unmerged MP => skip
 * Exactly one prerequisite non-superseded *Merged* MP => proceed

Additionally, the tarmac queue is processed taking into account prerequisite
branches. That is, dependent branches are processed *after* their prerequisites.

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

32 + * Multiple prerequisite non-superseded MPs => error

How does one declare multiple prerequisite branches in Launchpad?

161 +++ tarmac/tests/test_commands.py 2013-12-05 22:45:14 +0000

Many of the changes in here seem unrelated to the prerequisite work, and seems to be just you changing some of the names for the third branch in several of the tests. How are these related?

The bug you filed is a duplicate of https://bugs.launchpad.net/tarmac/+bug/900731 and I've marked it as such already. Changing the --fixes here too would be great, if possible.

Also, you need to commit using an e-mail address that is associated with your Launchpad account. Your shortened canonical.com address does not appear to be associated to your account, and so Launchpad cannot link to your account, nor will tarmac be able to find you via the Launchpad API from the e-mail address in the commit.

review: Needs Fixing
426. By David Britton

remove renaming of 'branch3'

Revision history for this message
David Britton (dpb) wrote :

> 32 + * Multiple prerequisite non-superseded MPs => error
>
> How does one declare multiple prerequisite branches in Launchpad?
>

I don't know, but I didn't change or add this code path. You can see it at line 231 of tarmac/bin/commands.py. I just documented what would happen if this condition was hit.

What would you like me to do?

> 161 +++ tarmac/tests/test_commands.py 2013-12-05 22:45:14 +0000
>
> Many of the changes in here seem unrelated to the prerequisite work, and seems
> to be just you changing some of the names for the third branch in several of
> the tests. How are these related?
>

I reverted since you asked. They were to help me track down which test was failing before adding the 'unique_name' property. The error message coming out of bzrlib was not helpful and just contained 'branch3' which made it hard to see which test failed.

> The bug you filed is a duplicate of
> https://bugs.launchpad.net/tarmac/+bug/900731 and I've marked it as such
> already. Changing the --fixes here too would be great, if possible.

OK, I'll hopefully add '--fixes' to this commit

> Also, you need to commit using an e-mail address that is associated with your
> Launchpad account. Your shortened canonical.com address does not appear to be
> associated to your account, and so Launchpad cannot link to your account, nor
> will tarmac be able to find you via the Launchpad API from the e-mail address
> in the commit.

I added it.

427. By David Britton

Adding lp:900731 to the list of fixes

Revision history for this message
dobey (dobey) wrote :

> > 32 + * Multiple prerequisite non-superseded MPs => error
> >
> > How does one declare multiple prerequisite branches in Launchpad?
> >
>
> I don't know, but I didn't change or add this code path. You can see it at
> line 231 of tarmac/bin/commands.py. I just documented what would happen if
> this condition was hit.
>
> What would you like me to do?

I think you've misunderstood the code. That code is to deal with the case when a prerequisite branch has multiple proposals (ie, if someone re-proposed the branch and there is a superseded proposal, and such). These are not multiple prerequisites. The comments and documentation need to be clear about what the code is actually doing.

Revision history for this message
David Britton (dpb) wrote :

I'll copy it here again:

"Multiple prerequisite non-superseded MPs"

What is wrong with that statement? I didn't say multiple prerequisite
branches. I said multiple prerequisite MPs (merge proposals). What would
you like me to say to clarify it?

Also, there is no comment about this. It's just documentation in a Readme,
unless I'm missing something.

On Sun, Feb 9, 2014 at 12:27 PM, Rodney Dawes <email address hidden>wrote:

> > > 32 + * Multiple prerequisite non-superseded MPs => error
> > >
> > > How does one declare multiple prerequisite branches in Launchpad?
> > >
> >
> > I don't know, but I didn't change or add this code path. You can see it
> at
> > line 231 of tarmac/bin/commands.py. I just documented what would happen
> if
> > this condition was hit.
> >
> > What would you like me to do?
>
> I think you've misunderstood the code. That code is to deal with the case
> when a prerequisite branch has multiple proposals (ie, if someone
> re-proposed the branch and there is a superseded proposal, and such). These
> are not multiple prerequisites. The comments and documentation need to be
> clear about what the code is actually doing.
> --
>
> https://code.launchpad.net/~davidpbritton/tarmac/better-prereq-branches/+merge/197969
> You are the owner of lp:~davidpbritton/tarmac/better-prereq-branches.
>

--
David Britton <email address hidden>

428. By David Britton

Clean up wording around prerequisite branches

Revision history for this message
David Britton (dpb) wrote :

I committed a rewording change, I hope it's more clear. I think calling an MP "prerequisite" was what was leading to the confusion. (hopefully)

Revision history for this message
dobey (dobey) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2010-07-24 13:04:41 +0000
3+++ .bzrignore 2014-02-09 23:07:13 +0000
4@@ -4,3 +4,4 @@
5 dist
6 TODO
7 debug*
8+tarmac.egg-info
9
10=== modified file 'docs/introduction.txt'
11--- docs/introduction.txt 2013-11-06 19:35:08 +0000
12+++ docs/introduction.txt 2014-02-09 23:07:13 +0000
13@@ -6,7 +6,6 @@
14 merge, it should really just magically be merged. That's what Tarmac does, and
15 it does it well.
16
17-
18 How it works
19 ============
20
21@@ -20,6 +19,21 @@
22 branch has landed. This opens up all sorts of branch landing options that can
23 happen behind the scenes, while you can safely do more exciting work.
24
25+Prerequisite Branches
26+=====================
27+
28+Tarmac understands the launchpad concept of prerequisite branches. The
29+following conditions are supported when evaluating a candidate merge
30+proposal (MP).
31+
32+ * Prerequisite branch without an associated MP => error
33+ * Prerequisite branch with multiple non-superseded MPs => error
34+ * Prerequisite branch with exactly one non-superseded unmerged MP => skip
35+ * Prerequisite branch with exactly one non-superseded *Merged* MP => proceed
36+
37+Additionally, the tarmac queue is processed taking into account prerequisite
38+branches. That is, dependent branches are processed *after* their
39+prerequisites.
40
41 Configuration
42 =============
43
44=== modified file 'tarmac/bin/commands.py'
45--- tarmac/bin/commands.py 2013-11-07 17:28:53 +0000
46+++ tarmac/bin/commands.py 2014-02-09 23:07:13 +0000
47@@ -25,6 +25,17 @@
48 from tarmac.utility import get_review_url
49
50
51+def _compare_proposals(a, b):
52+ """Helper to sort proposals based on a prerequisite branch"""
53+ if a.prerequisite_branch is not None:
54+ if a.prerequisite_branch.unique_name == b.source_branch.unique_name:
55+ return 1
56+ if b.prerequisite_branch is not None:
57+ if b.prerequisite_branch.unique_name == a.source_branch.unique_name:
58+ return -1
59+ return 0
60+
61+
62 class TarmacCommand(Command):
63 '''A command class.'''
64
65@@ -210,9 +221,7 @@
66 try:
67 prerequisite = proposal.prerequisite_branch
68 if prerequisite:
69- merges = [x for x in prerequisite.landing_targets
70- if x.target_branch == target.lp_branch and
71- x.queue_status != u'Superseded']
72+ merges = self._get_prerequisite_proposals(proposal)
73 if len(merges) == 0:
74 raise TarmacMergeError(
75 u'No proposals of prerequisite branch.',
76@@ -227,14 +236,6 @@
77 u'of %s into %s, which is not Superseded.' % (
78 prerequisite.web_link,
79 target.lp_branch.web_link))
80- elif len(merges) == 1:
81- if merges[0].queue_status != u'Merged':
82- raise TarmacMergeError(
83- u'Prerequisite not yet merged.',
84- u'The prerequisite %s has not yet been '
85- u'merged into %s.' % (
86- prerequisite.web_link,
87- target.lp_branch.web_link))
88
89 if not proposal.reviewed_revid:
90 raise TarmacMergeError(
91@@ -325,10 +326,16 @@
92 target.cleanup()
93
94 def _get_mergable_proposals_for_branch(self, lp_branch):
95- """Return a list of the mergable proposals for the given branch."""
96+ """
97+ Return a list of the mergable proposals for the given branch. The
98+ list returned will be in the order that they should be processed.
99+ """
100 proposals = []
101- for entry in lp_branch.landing_candidates:
102+ sorted_proposals = sorted(
103+ lp_branch.landing_candidates, cmp=_compare_proposals)
104+ for entry in sorted_proposals:
105 self.logger.debug("Considering merge proposal: {0}".format(entry.web_link))
106+ prereqs = self._get_prerequisite_proposals(entry)
107
108 if entry.queue_status != u'Approved':
109 self.logger.debug(
110@@ -342,9 +349,31 @@
111 " Skipping proposal: proposal has no commit message")
112 continue
113
114+ if len(prereqs) == 1 and prereqs[0].queue_status != u'Merged':
115+ # N.B.: The case of a MP with more than one prereq MP open
116+ # will be caught as a merge error.
117+ self.logger.debug(
118+ " Skipping proposal: prerequisite not yet merged")
119+ continue
120+
121 proposals.append(entry)
122 return proposals
123
124+ def _get_prerequisite_proposals(self, proposal):
125+ """
126+ Given a proposal, return all prerequisite
127+ proposals that are not in the superseded state. There ideally
128+ should be one and only one here (or zero), but sometimes there
129+ are not, depending on developer habits
130+ """
131+ prerequisite = proposal.prerequisite_branch
132+ target_branch = proposal.target_branch
133+ if not prerequisite or not prerequisite.landing_targets:
134+ return []
135+ return [x for x in prerequisite.landing_targets
136+ if x.target_branch.unique_name == target_branch.unique_name and
137+ x.queue_status != u'Superseded']
138+
139 def _get_reviews(self, proposal):
140 """Get the set of reviews from the proposal."""
141 reviews = []
142@@ -405,3 +434,4 @@
143 'An error occurred trying to merge %s: %s',
144 branch, error)
145 raise
146+
147
148=== modified file 'tarmac/tests/__init__.py'
149--- tarmac/tests/__init__.py 2013-10-31 19:09:08 +0000
150+++ tarmac/tests/__init__.py 2014-02-09 23:07:13 +0000
151@@ -56,6 +56,7 @@
152 self.revision_count = 0
153 self.bzr_identity = 'lp:%s' % os.path.basename(self.tree_dir)
154 self.web_link = self.bzr_identity
155+ self.unique_name = self.bzr_identity
156 self.project = MockLPProject()
157
158
159
160=== modified file 'tarmac/tests/test_commands.py'
161--- tarmac/tests/test_commands.py 2013-11-07 17:28:53 +0000
162+++ tarmac/tests/test_commands.py 2014-02-09 23:07:13 +0000
163@@ -128,13 +128,16 @@
164 display_name=self.branch2.lp_branch.bzr_identity,
165 web_link=self.branch2.lp_branch.bzr_identity,
166 name='source',
167+ unique_name='source',
168 revision_count=self.branch2.lp_branch.revision_count,
169- landing_candidates=[]),
170+ landing_candidates=[],
171+ landing_targets=[]),
172 Thing(
173 bzr_identity=self.branch1.lp_branch.bzr_identity,
174 display_name=self.branch1.lp_branch.bzr_identity,
175 web_link=self.branch1.lp_branch.bzr_identity,
176 name='target',
177+ unique_name='target',
178 revision_count=self.branch1.lp_branch.revision_count,
179 landing_candidates=None)]
180 self.proposals = [Thing(
181@@ -171,6 +174,8 @@
182 comment=Thing(vote=u'Abstain'),
183 reviewer=Thing(display_name=u'Reviewer2'))])]
184 self.branches[1].landing_candidates = self.proposals
185+ self.branches[0].landing_targets = [
186+ self.proposals[0], self.proposals[1]]
187
188 self.launchpad = Thing(branches=Thing(getByUrl=self.getBranchByUrl),
189 me=Thing(display_name='Tarmac'))
190@@ -180,6 +185,42 @@
191
192 self.command = registry._get_command(commands.cmd_merge, 'merge')
193
194+ def addProposal(self, name, prerequisite_branch=None):
195+ """Create a 3rd branch with a proposal"""
196+ # Create a 3rd branch we'll use to test with
197+ branch3_dir = os.path.join(self.TEST_ROOT, name)
198+ mock3 = MockLPBranch(branch3_dir, source_branch=self.branch1.lp_branch)
199+ branch3 = Branch.create(mock3, self.config, create_tree=True,
200+ target=self.branch1)
201+ branch3.commit('Prerequisite commit.')
202+ branch3.lp_branch.revision_count += 1
203+
204+ # Set up an approved proposal for the branch (prereq on branches[0])
205+ branch3.lp_branch.display_name = branch3.lp_branch.bzr_identity
206+ branch3.lp_branch.name = name
207+ branch3.lp_branch.unique_name = '~user/branch/' + name
208+ branch3.lp_branch.landing_candidates = []
209+ b3_proposal = Thing(
210+ self_link=u'http://api.edge.launchpad.net/devel/proposal3',
211+ web_link=u'http://edge.launchpad.net/proposal3',
212+ queue_status=u'Approved',
213+ commit_message=u'Commitable.',
214+ source_branch=branch3.lp_branch,
215+ target_branch=self.branches[1],
216+ prerequisite_branch=prerequisite_branch,
217+ createComment=self.createComment,
218+ setStatus=self.lp_save,
219+ lp_save=self.lp_save,
220+ reviewed_revid=None,
221+ votes=[Thing(
222+ comment=Thing(vote=u'Approve'),
223+ reviewer=Thing(display_name=u'Reviewer'))])
224+
225+ branch3.lp_branch.landing_targets = [b3_proposal]
226+ self.proposals.append(b3_proposal)
227+ self.branches.append(branch3.lp_branch)
228+
229+
230 def lp_save(self, *args, **kwargs):
231 """Do nothing here."""
232 pass
233@@ -248,8 +289,8 @@
234 self.assertEqual(last_rev.properties.get('merge_url', None),
235 u'http://code.launchpad.net/proposal0')
236
237- def test_run_merge_with_unmerged_prerequisite_fails(self):
238- """Test that mereging a branch with an unmerged prerequisite fails."""
239+ def test_run_merge_with_unmerged_prerequisite_skips(self):
240+ """Test that mereging a branch with an unmerged prerequisite skips."""
241 # Create a 3rd prerequisite branch we'll use to test with
242 branch3_dir = os.path.join(self.TEST_ROOT, 'branch3')
243 mock3 = MockLPBranch(branch3_dir, source_branch=self.branch1.lp_branch)
244@@ -267,6 +308,7 @@
245 # Set up an unapproved proposal for the prerequisite
246 branch3.lp_branch.display_name = branch3.lp_branch.bzr_identity
247 branch3.lp_branch.name = 'branch3'
248+ branch3.unique_name = 'branch3'
249 branch3.lp_branch.landing_candidates = []
250 b3_proposal = Thing(
251 self_link=u'http://api.edge.launchpad.net/devel/proposal3',
252@@ -290,11 +332,8 @@
253 self.proposals[1].prerequisite_branch = branch3.lp_branch
254 self.proposals[1].reviewed_revid = \
255 self.branch2.bzr_branch.last_revision()
256- self.command.run(launchpad=self.launchpad)
257+ self.assertEqual(self.command.run(launchpad=self.launchpad), None)
258 shutil.rmtree(branch3_dir)
259- self.assertEqual(self.error.comment,
260- u'The prerequisite lp:branch3 has not yet been '
261- u'merged into lp:branch1.')
262
263 def test_run_merge_with_unproposed_prerequisite_fails(self):
264 """Test that mereging a branch with an unmerged prerequisite fails."""
265@@ -315,6 +354,7 @@
266 # Set up an unapproved proposal for the prerequisite
267 branch3.lp_branch.display_name = branch3.lp_branch.bzr_identity
268 branch3.lp_branch.name = 'branch3'
269+ branch3.lp_branch.unique_name = 'branch3'
270 branch3.lp_branch.landing_candidates = []
271 b3_proposal = Thing(
272 self_link=u'http://api.edge.launchpad.net/devel/proposal3',
273@@ -363,6 +403,7 @@
274 # Set up an unapproved proposal for the prerequisite
275 branch3.lp_branch.display_name = branch3.lp_branch.bzr_identity
276 branch3.lp_branch.name = 'branch3'
277+ branch3.lp_branch.unique_name = 'branch3'
278 branch3.lp_branch.landing_candidates = []
279 b3_proposal = Thing(
280 self_link=u'http://api.edge.launchpad.net/devel/proposal3',
281@@ -394,8 +435,8 @@
282 shutil.rmtree(branch3_dir)
283 self.assertEqual(self.error.comment,
284 u'More than one proposal found for merge of '
285- u'lp:branch3 into lp:branch1, which is not '
286- u'Superseded.')
287+ u'lp:branch3 into lp:branch1, '
288+ u'which is not Superseded.')
289
290 @patch('bzrlib.workingtree.WorkingTree.open')
291 def test_run_merge_with_invalid_workingtree(self, mocked):
292@@ -407,3 +448,55 @@
293 self.command.run(launchpad=self.launchpad)
294 self.assertEqual(self.error.comment,
295 invalid_tree_comment)
296+
297+ def test__compare_proposals(self):
298+ """
299+ _compare_proposals is meant to be a sort routine comparison function
300+ """
301+ self.addProposal("compare_proposals", self.branches[0])
302+ self.assertEqual(
303+ commands._compare_proposals(self.proposals[0], self.proposals[1]),
304+ 0)
305+ self.assertEqual(
306+ commands._compare_proposals(self.proposals[0], self.proposals[2]),
307+ -1)
308+ self.assertEqual(
309+ commands._compare_proposals(self.proposals[2], self.proposals[0]),
310+ 1)
311+
312+ def test__get_mergable_proposals_for_branch_are_sorted(self):
313+ """
314+ Mergable proposals should be in sorted order (prereqs should come
315+ first in the list)
316+ """
317+ self.addProposal("sorted_test", self.branches[0])
318+ proposals = self.command._get_mergable_proposals_for_branch(
319+ self.branches[1])
320+ self.assertEqual(len(proposals), 2)
321+ self.assertTrue(proposals[1].source_branch.name == "sorted_test")
322+
323+ def test__get_mergable_proposals_for_branch_prereq_unmerged(self):
324+ """
325+ skip mergable proposals that have specified a prereq,
326+ but that prereq is not merged yet. This is edge-casey, since
327+ we now process the queue in an order that makes sense for
328+ prereqs
329+ """
330+ self.addProposal("unmerged")
331+ self.proposals[0].prerequisite_branch = self.branches[2]
332+ self.proposals[1].prerequisite_branch = self.branches[2]
333+ proposals = self.command._get_mergable_proposals_for_branch(
334+ self.branches[1])
335+ self.assertEqual(len(proposals), 1)
336+ self.assertEqual(proposals[0].source_branch.name, "unmerged")
337+
338+ def test__get_prerequisite_proposals_no_prerequisites(self):
339+ """proposals[0] does not have a prerequisite branch listed"""
340+ proposals = self.command._get_prerequisite_proposals(self.proposals[0])
341+ self.assertEqual(len(proposals), 0)
342+
343+ def test__get_prerequisite_proposals_one_prerequisite(self):
344+ """Branches[0] (source) has two open MPs against it"""
345+ self.addProposal("one_prerequisite", self.branches[0])
346+ proposals = self.command._get_prerequisite_proposals(self.proposals[2])
347+ self.assertEqual(len(proposals), 2)

Subscribers

People subscribed via source and target branches