Status: | Merged |
---|---|
Merged at revision: | not available |
Proposed branch: | lp:~bac/launchpad/gmp |
Merge into: | lp:launchpad |
Diff against target: |
251 lines 3 files modified
lib/devscripts/autoland.py (+58/-7) lib/devscripts/ec2test/builtins.py (+25/-10) lib/devscripts/tests/test_autoland.py (+12/-1) |
To merge this branch: | bzr merge lp:~bac/launchpad/gmp |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Paul Hummer (community) | code | Approve | |
Canonical Launchpad Engineering | Pending | ||
Review via email: mp+13083@code.launchpad.net |
Commit message
Make 'ec2 land' figure out where to find the merge proposal so that it is optional.
Description of the change
To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote : | # |
Revision history for this message
Paul Hummer (rockstar) : | # |
review:
Approve
(ui)
Revision history for this message
Paul Hummer (rockstar) : | # |
review:
Approve
(code)
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lib/devscripts/autoland.py' | |||
2 | --- lib/devscripts/autoland.py 2009-09-30 12:34:02 +0000 | |||
3 | +++ lib/devscripts/autoland.py 2009-10-08 23:25:23 +0000 | |||
4 | @@ -5,6 +5,8 @@ | |||
5 | 5 | from launchpadlib.launchpad import ( | 5 | from launchpadlib.launchpad import ( |
6 | 6 | Launchpad, EDGE_SERVICE_ROOT, STAGING_SERVICE_ROOT) | 6 | Launchpad, EDGE_SERVICE_ROOT, STAGING_SERVICE_ROOT) |
7 | 7 | from lazr.uri import URI | 7 | from lazr.uri import URI |
8 | 8 | from bzrlib.errors import BzrCommandError | ||
9 | 9 | |||
10 | 8 | 10 | ||
11 | 9 | # XXX: JonathanLange 2009-09-24: Both of these are available in more recent | 11 | # XXX: JonathanLange 2009-09-24: Both of these are available in more recent |
12 | 10 | # versions of launchpadlib. When we start using such versions, we should | 12 | # versions of launchpadlib. When we start using such versions, we should |
13 | @@ -42,6 +44,42 @@ | |||
14 | 42 | web_mp_uri.path.lstrip('/')) | 44 | web_mp_uri.path.lstrip('/')) |
15 | 43 | return MergeProposal(self._launchpad.load(str(api_mp_uri))) | 45 | return MergeProposal(self._launchpad.load(str(api_mp_uri))) |
16 | 44 | 46 | ||
17 | 47 | def get_lp_branch(self, branch): | ||
18 | 48 | """Get the launchpadlib branch based on a bzr branch.""" | ||
19 | 49 | # First try the public branch. | ||
20 | 50 | branch_url = branch.get_public_branch() | ||
21 | 51 | if branch_url: | ||
22 | 52 | lp_branch = self._launchpad.branches.getByUrl( | ||
23 | 53 | url=branch_url) | ||
24 | 54 | if lp_branch is not None: | ||
25 | 55 | return lp_branch | ||
26 | 56 | # If that didn't work try the push location. | ||
27 | 57 | branch_url = branch.get_push_location() | ||
28 | 58 | if branch_url: | ||
29 | 59 | lp_branch = self._launchpad.branches.getByUrl( | ||
30 | 60 | url=branch_url) | ||
31 | 61 | if lp_branch is not None: | ||
32 | 62 | return lp_branch | ||
33 | 63 | raise BzrCommandError( | ||
34 | 64 | "No public branch could be found. Please re-run and specify " | ||
35 | 65 | "the URL for the merge proposal.") | ||
36 | 66 | |||
37 | 67 | def get_merge_proposal_from_branch(self, branch): | ||
38 | 68 | """Get the merge proposal from the branch.""" | ||
39 | 69 | |||
40 | 70 | lp_branch = self.get_lp_branch(branch) | ||
41 | 71 | proposals = lp_branch.landing_targets | ||
42 | 72 | if len(proposals) == 0: | ||
43 | 73 | raise BzrCommandError( | ||
44 | 74 | "The public branch has no source merge proposals. " | ||
45 | 75 | "You must have a merge proposal before attempting to " | ||
46 | 76 | "land the branch.") | ||
47 | 77 | elif len(proposals) > 1: | ||
48 | 78 | raise BzrCommandError( | ||
49 | 79 | "The public branch has multiple source merge proposals. " | ||
50 | 80 | "You must provide the URL to the one you wish to use.") | ||
51 | 81 | return MergeProposal(proposals[0]) | ||
52 | 82 | |||
53 | 45 | 83 | ||
54 | 46 | class MergeProposal: | 84 | class MergeProposal: |
55 | 47 | """Wrapper around launchpadlib `IBranchMergeProposal` for landing.""" | 85 | """Wrapper around launchpadlib `IBranchMergeProposal` for landing.""" |
56 | @@ -83,7 +121,8 @@ | |||
57 | 83 | """ | 121 | """ |
58 | 84 | # XXX: JonathanLange 2009-09-24: No unit tests. | 122 | # XXX: JonathanLange 2009-09-24: No unit tests. |
59 | 85 | return set( | 123 | return set( |
61 | 86 | map(get_email, [self._mp.source_branch.owner, self._launchpad.me])) | 124 | map(get_email, |
62 | 125 | [self._mp.source_branch.owner, self._launchpad.me])) | ||
63 | 87 | 126 | ||
64 | 88 | def get_reviews(self): | 127 | def get_reviews(self): |
65 | 89 | """Return a dictionary of all Approved reviews. | 128 | """Return a dictionary of all Approved reviews. |
66 | @@ -177,8 +216,11 @@ | |||
67 | 177 | 216 | ||
68 | 178 | 217 | ||
69 | 179 | def _comma_separated_names(things): | 218 | def _comma_separated_names(things): |
72 | 180 | """Return a string of comma-separated names of 'things'.""" | 219 | """Return a string of comma-separated names of 'things'. |
73 | 181 | return ','.join(thing.name for thing in things) | 220 | |
74 | 221 | The list is sorted before being joined. | ||
75 | 222 | """ | ||
76 | 223 | return ','.join(sorted(thing.name for thing in things)) | ||
77 | 182 | 224 | ||
78 | 183 | 225 | ||
79 | 184 | def get_reviewer_clause(reviewers): | 226 | def get_reviewer_clause(reviewers): |
80 | @@ -188,17 +230,26 @@ | |||
81 | 188 | returned by 'get_reviews'. | 230 | returned by 'get_reviews'. |
82 | 189 | :return: A string like u'[r=foo,bar][ui=plop]'. | 231 | :return: A string like u'[r=foo,bar][ui=plop]'. |
83 | 190 | """ | 232 | """ |
84 | 233 | # If no review type is specified it will be None and is assumed to be a | ||
85 | 234 | # code review. | ||
86 | 191 | code_reviewers = reviewers.get(None, []) | 235 | code_reviewers = reviewers.get(None, []) |
89 | 192 | code_reviewers.extend(reviewers.get('code', [])) | 236 | ui_reviewers = [] |
90 | 193 | code_reviewers.extend(reviewers.get('db', [])) | 237 | rc_reviewers = [] |
91 | 238 | for review_type, reviewer in reviewers.items(): | ||
92 | 239 | if review_type is None: | ||
93 | 240 | continue | ||
94 | 241 | if 'code' in review_type or 'db' in review_type: | ||
95 | 242 | code_reviewers.extend(reviewer) | ||
96 | 243 | if 'ui' in review_type: | ||
97 | 244 | ui_reviewers.extend(reviewer) | ||
98 | 245 | if 'release-critical' in review_type: | ||
99 | 246 | rc_reviewers.extend(reviewer) | ||
100 | 194 | if not code_reviewers: | 247 | if not code_reviewers: |
101 | 195 | raise MissingReviewError("Need approved votes in order to land.") | 248 | raise MissingReviewError("Need approved votes in order to land.") |
102 | 196 | ui_reviewers = reviewers.get('ui', []) | ||
103 | 197 | if ui_reviewers: | 249 | if ui_reviewers: |
104 | 198 | ui_clause = _comma_separated_names(ui_reviewers) | 250 | ui_clause = _comma_separated_names(ui_reviewers) |
105 | 199 | else: | 251 | else: |
106 | 200 | ui_clause = 'none' | 252 | ui_clause = 'none' |
107 | 201 | rc_reviewers = reviewers.get('release-critical', []) | ||
108 | 202 | if rc_reviewers: | 253 | if rc_reviewers: |
109 | 203 | rc_clause = ( | 254 | rc_clause = ( |
110 | 204 | '[release-critical=%s]' % _comma_separated_names(rc_reviewers)) | 255 | '[release-critical=%s]' % _comma_separated_names(rc_reviewers)) |
111 | 205 | 256 | ||
112 | === modified file 'lib/devscripts/ec2test/builtins.py' | |||
113 | --- lib/devscripts/ec2test/builtins.py 2009-10-05 13:31:40 +0000 | |||
114 | +++ lib/devscripts/ec2test/builtins.py 2009-10-08 23:25:23 +0000 | |||
115 | @@ -10,6 +10,7 @@ | |||
116 | 10 | import pdb | 10 | import pdb |
117 | 11 | import subprocess | 11 | import subprocess |
118 | 12 | 12 | ||
119 | 13 | from bzrlib.bzrdir import BzrDir | ||
120 | 13 | from bzrlib.commands import Command | 14 | from bzrlib.commands import Command |
121 | 14 | from bzrlib.errors import BzrCommandError | 15 | from bzrlib.errors import BzrCommandError |
122 | 15 | from bzrlib.help import help_commands | 16 | from bzrlib.help import help_commands |
123 | @@ -240,8 +241,8 @@ | |||
124 | 240 | postmortem_option, | 241 | postmortem_option, |
125 | 241 | Option( | 242 | Option( |
126 | 242 | 'headless', | 243 | 'headless', |
129 | 243 | help=('After building the instance and test, run the remote tests ' | 244 | help=('After building the instance and test, run the remote ' |
130 | 244 | 'headless. Cannot be used with postmortem ' | 245 | 'tests headless. Cannot be used with postmortem ' |
131 | 245 | 'or file.')), | 246 | 'or file.')), |
132 | 246 | debug_option, | 247 | debug_option, |
133 | 247 | Option( | 248 | Option( |
134 | @@ -252,7 +253,7 @@ | |||
135 | 252 | 253 | ||
136 | 253 | takes_args = ['test_branch?'] | 254 | takes_args = ['test_branch?'] |
137 | 254 | 255 | ||
139 | 255 | def run(self, test_branch=None, branch=[], trunk=False, machine=None, | 256 | def run(self, test_branch=None, branch=None, trunk=False, machine=None, |
140 | 256 | instance_type=DEFAULT_INSTANCE_TYPE, | 257 | instance_type=DEFAULT_INSTANCE_TYPE, |
141 | 257 | file=None, email=None, test_options='-vv', noemail=False, | 258 | file=None, email=None, test_options='-vv', noemail=False, |
142 | 258 | submit_pqm_message=None, pqm_public_location=None, | 259 | submit_pqm_message=None, pqm_public_location=None, |
143 | @@ -261,6 +262,8 @@ | |||
144 | 261 | include_download_cache_changes=False): | 262 | include_download_cache_changes=False): |
145 | 262 | if debug: | 263 | if debug: |
146 | 263 | pdb.set_trace() | 264 | pdb.set_trace() |
147 | 265 | if branch is None: | ||
148 | 266 | branch = [] | ||
149 | 264 | branches, test_branch = _get_branches_and_test_branch( | 267 | branches, test_branch = _get_branches_and_test_branch( |
150 | 265 | trunk, branch, test_branch) | 268 | trunk, branch, test_branch) |
151 | 266 | if ((postmortem or file) and headless): | 269 | if ((postmortem or file) and headless): |
152 | @@ -321,7 +324,7 @@ | |||
153 | 321 | help="Land the branch even if the proposal is not approved."), | 324 | help="Land the branch even if the proposal is not approved."), |
154 | 322 | ] | 325 | ] |
155 | 323 | 326 | ||
157 | 324 | takes_args = ['merge_proposal'] | 327 | takes_args = ['merge_proposal?'] |
158 | 325 | 328 | ||
159 | 326 | def _get_landing_command(self, source_url, target_url, commit_message, | 329 | def _get_landing_command(self, source_url, target_url, commit_message, |
160 | 327 | emails): | 330 | emails): |
161 | @@ -338,7 +341,7 @@ | |||
162 | 338 | commit_message, str(source_url)]) | 341 | commit_message, str(source_url)]) |
163 | 339 | return command | 342 | return command |
164 | 340 | 343 | ||
166 | 341 | def run(self, merge_proposal, machine=None, | 344 | def run(self, merge_proposal=None, machine=None, |
167 | 342 | instance_type=DEFAULT_INSTANCE_TYPE, postmortem=False, | 345 | instance_type=DEFAULT_INSTANCE_TYPE, postmortem=False, |
168 | 343 | debug=False, commit_text=None, dry_run=False, testfix=False, | 346 | debug=False, commit_text=None, dry_run=False, testfix=False, |
169 | 344 | print_commit=False, force=False): | 347 | print_commit=False, force=False): |
170 | @@ -361,7 +364,13 @@ | |||
171 | 361 | raise BzrCommandError( | 364 | raise BzrCommandError( |
172 | 362 | "Cannot specify --print-commit and --dry-run.") | 365 | "Cannot specify --print-commit and --dry-run.") |
173 | 363 | lander = LaunchpadBranchLander.load() | 366 | lander = LaunchpadBranchLander.load() |
175 | 364 | mp = lander.load_merge_proposal(merge_proposal) | 367 | |
176 | 368 | if merge_proposal is None: | ||
177 | 369 | (tree, bzrbranch, relpath) = ( | ||
178 | 370 | BzrDir.open_containing_tree_or_branch('.')) | ||
179 | 371 | mp = lander.get_merge_proposal_from_branch(bzrbranch) | ||
180 | 372 | else: | ||
181 | 373 | mp = lander.load_merge_proposal(merge_proposal) | ||
182 | 365 | if not mp.is_approved: | 374 | if not mp.is_approved: |
183 | 366 | if force: | 375 | if force: |
184 | 367 | print "Merge proposal is not approved, landing anyway." | 376 | print "Merge proposal is not approved, landing anyway." |
185 | @@ -418,11 +427,13 @@ | |||
186 | 418 | 427 | ||
187 | 419 | takes_args = ['test_branch?'] | 428 | takes_args = ['test_branch?'] |
188 | 420 | 429 | ||
190 | 421 | def run(self, test_branch=None, branch=[], trunk=False, machine=None, | 430 | def run(self, test_branch=None, branch=None, trunk=False, machine=None, |
191 | 422 | instance_type=DEFAULT_INSTANCE_TYPE, debug=False, | 431 | instance_type=DEFAULT_INSTANCE_TYPE, debug=False, |
192 | 423 | include_download_cache_changes=False, demo=None): | 432 | include_download_cache_changes=False, demo=None): |
193 | 424 | if debug: | 433 | if debug: |
194 | 425 | pdb.set_trace() | 434 | pdb.set_trace() |
195 | 435 | if branch is None: | ||
196 | 436 | branch = [] | ||
197 | 426 | branches, test_branch = _get_branches_and_test_branch( | 437 | branches, test_branch = _get_branches_and_test_branch( |
198 | 427 | trunk, branch, test_branch) | 438 | trunk, branch, test_branch) |
199 | 428 | 439 | ||
200 | @@ -480,8 +491,9 @@ | |||
201 | 480 | ListOption( | 491 | ListOption( |
202 | 481 | 'extra-update-image-command', type=str, | 492 | 'extra-update-image-command', type=str, |
203 | 482 | help=('Run this command (with an ssh agent) on the image before ' | 493 | help=('Run this command (with an ssh agent) on the image before ' |
206 | 483 | 'running the default update steps. Can be passed more than ' | 494 | 'running the default update steps. Can be passed more ' |
207 | 484 | 'once, the commands will be run in the order specified.')), | 495 | 'than once, the commands will be run in the order ' |
208 | 496 | 'specified.')), | ||
209 | 485 | Option( | 497 | Option( |
210 | 486 | 'public', | 498 | 'public', |
211 | 487 | help=('Remove proprietary code from the sourcecode directory ' | 499 | help=('Remove proprietary code from the sourcecode directory ' |
212 | @@ -491,11 +503,14 @@ | |||
213 | 491 | takes_args = ['ami_name'] | 503 | takes_args = ['ami_name'] |
214 | 492 | 504 | ||
215 | 493 | def run(self, ami_name, machine=None, instance_type='m1.large', | 505 | def run(self, ami_name, machine=None, instance_type='m1.large', |
217 | 494 | debug=False, postmortem=False, extra_update_image_command=[], | 506 | debug=False, postmortem=False, extra_update_image_command=None, |
218 | 495 | public=False): | 507 | public=False): |
219 | 496 | if debug: | 508 | if debug: |
220 | 497 | pdb.set_trace() | 509 | pdb.set_trace() |
221 | 498 | 510 | ||
222 | 511 | if extra_update_image_command is None: | ||
223 | 512 | extra_update_image_command = [] | ||
224 | 513 | |||
225 | 499 | credentials = EC2Credentials.load_from_file() | 514 | credentials = EC2Credentials.load_from_file() |
226 | 500 | 515 | ||
227 | 501 | instance = EC2Instance.make( | 516 | instance = EC2Instance.make( |
228 | 502 | 517 | ||
229 | === modified file 'lib/devscripts/tests/test_autoland.py' | |||
230 | --- lib/devscripts/tests/test_autoland.py 2009-09-24 11:23:24 +0000 | |||
231 | +++ lib/devscripts/tests/test_autoland.py 2009-10-08 23:25:23 +0000 | |||
232 | @@ -114,7 +114,18 @@ | |||
233 | 114 | # Branches can have more than one reviewer. | 114 | # Branches can have more than one reviewer. |
234 | 115 | clause = self.get_reviewer_clause( | 115 | clause = self.get_reviewer_clause( |
235 | 116 | {None: [self.makePerson('foo'), self.makePerson('bar')]}) | 116 | {None: [self.makePerson('foo'), self.makePerson('bar')]}) |
237 | 117 | self.assertEqual('[r=foo,bar][ui=none]', clause) | 117 | self.assertEqual('[r=bar,foo][ui=none]', clause) |
238 | 118 | |||
239 | 119 | def test_mentat_reviewers(self): | ||
240 | 120 | # A mentat review sometimes is marked like 'ui*'. Due to the | ||
241 | 121 | # unordered nature of dictionaries, the reviewers are sorted before | ||
242 | 122 | # being put into the clause for predictability. | ||
243 | 123 | clause = self.get_reviewer_clause( | ||
244 | 124 | {None: [self.makePerson('foo')], | ||
245 | 125 | 'code*': [self.makePerson('newguy')], | ||
246 | 126 | 'ui': [self.makePerson('beuno')], | ||
247 | 127 | 'ui*': [self.makePerson('bac')]}) | ||
248 | 128 | self.assertEqual('[r=foo,newguy][ui=bac,beuno]', clause) | ||
249 | 118 | 129 | ||
250 | 119 | def test_code_reviewer_counts(self): | 130 | def test_code_reviewer_counts(self): |
251 | 120 | # Some people explicitly specify the 'code' type when they do code | 131 | # Some people explicitly specify the 'code' type when they do code |
= Summary =
'ec2 land' is a great improvement in submitting branches via ec2 for landing as it
figures out all of the good parts and creates the PQM submit message. The original
version, however, requires the URL to the merge proposal as an argument. There is no
reason for requiring the MP as it can be found by using the public branch or push
location.
== Proposed fix ==
Make the merge proposal optional and if there is exactly one merge proposal for the
branch use it.
With this fix, if a developer is sitting in a branch that has an approved MP, she can
send it off for test and landing by just doing:
% ec2 land
Presto bingo!
== Pre-implementation notes ==
Talked with Jonathan Lange and got good suggestions on how to make it more robust,
including falling back on the push location.
== Implementation details ==
None really.
== Tests ==
Erm...
== Demo and Q/A ==
Go to the directory for a branch that has a merge proposal in 'Approved' state and try:
% ec2 land --dry-run
= Launchpad lint =
Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.
Linting changed files: s/autoland. py s/ec2test/ builtins. py
lib/devscript
lib/devscript
== Pylint notices ==
lib/devscripts/ autoland. py
124: [C0301] Line too long (79/78)
lib/devscripts/ ec2test/ builtins. py image.run] Dangerous default value [] as argument
244: [C0301] Line too long (79/78)
490: [C0301] Line too long (79/78)
256: [W0102, cmd_test.run] Dangerous default value [] as argument
428: [W0102, cmd_demo.run] Dangerous default value [] as argument
500: [W0102, cmd_update_
I'll fix these before landing as appropriate.