Merge lp:~jelmer/brz/my-proposals into lp:brz
- my-proposals
- Merge into trunk
Proposed by
Jelmer Vernooij
Status: | Merged |
---|---|
Approved by: | Jelmer Vernooij |
Approved revision: | no longer in the source branch. |
Merge reported by: | The Breezy Bot |
Merged at revision: | not available |
Proposed branch: | lp:~jelmer/brz/my-proposals |
Merge into: | lp:brz |
Diff against target: |
795 lines (+291/-104) 6 files modified
breezy/plugins/propose/__init__.py (+3/-0) breezy/plugins/propose/cmds.py (+29/-2) breezy/plugins/propose/github.py (+44/-6) breezy/plugins/propose/gitlabs.py (+78/-25) breezy/plugins/propose/launchpad.py (+108/-58) breezy/plugins/propose/propose.py (+29/-13) |
To merge this branch: | bzr merge lp:~jelmer/brz/my-proposals |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Martin Packman | Approve | ||
Review via email: mp+361363@code.launchpad.net |
Commit message
Add 'bzr my-proposals' command.
Description of the change
Add 'bzr my-proposals' command.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'breezy/plugins/propose/__init__.py' |
2 | --- breezy/plugins/propose/__init__.py 2018-12-11 14:12:47 +0000 |
3 | +++ breezy/plugins/propose/__init__.py 2019-01-08 21:33:41 +0000 |
4 | @@ -25,3 +25,6 @@ |
5 | plugin_cmds.register_lazy("cmd_find_merge_proposal", ['find-proposal'], __name__ + ".cmds") |
6 | plugin_cmds.register_lazy("cmd_github_login", ["gh-login"], __name__ + ".cmds") |
7 | plugin_cmds.register_lazy("cmd_gitlab_login", ["gl-login"], __name__ + ".cmds") |
8 | +plugin_cmds.register_lazy( |
9 | + "cmd_my_merge_proposals", ["my-proposals"], |
10 | + __name__ + ".cmds") |
11 | |
12 | === modified file 'breezy/plugins/propose/cmds.py' |
13 | --- breezy/plugins/propose/cmds.py 2019-01-01 22:31:13 +0000 |
14 | +++ breezy/plugins/propose/cmds.py 2019-01-08 21:33:41 +0000 |
15 | @@ -223,8 +223,8 @@ |
16 | else: |
17 | target = _mod_branch.Branch.open(submit_branch) |
18 | hoster = _mod_propose.get_hoster(branch) |
19 | - mp = hoster.get_proposal(branch, target) |
20 | - self.outf.write(gettext('Merge proposal: %s\n') % mp.url) |
21 | + for mp in hoster.iter_proposals(branch, target): |
22 | + self.outf.write(gettext('Merge proposal: %s\n') % mp.url) |
23 | |
24 | |
25 | class cmd_github_login(Command): |
26 | @@ -300,3 +300,30 @@ |
27 | gl = Gitlab(url=url, private_token=private_token) |
28 | gl.auth() |
29 | store_gitlab_token(name=name, url=url, private_token=private_token) |
30 | + |
31 | + |
32 | +class cmd_my_merge_proposals(Command): |
33 | + __doc__ = """List all merge proposals owned by the logged-in user. |
34 | + |
35 | + """ |
36 | + |
37 | + hidden = True |
38 | + |
39 | + takes_options = [ |
40 | + RegistryOption.from_kwargs( |
41 | + 'status', |
42 | + title='Proposal Status', |
43 | + help='Only include proposals with specified status.', |
44 | + value_switches=True, |
45 | + enum_switch=True, |
46 | + all='All merge proposals', |
47 | + open='Open merge proposals', |
48 | + merged='Merged merge proposals', |
49 | + closed='Closed merge proposals')] |
50 | + |
51 | + def run(self, status='open'): |
52 | + from .propose import hosters |
53 | + for name, hoster_cls in hosters.items(): |
54 | + for instance in hoster_cls.iter_instances(): |
55 | + for mp in instance.iter_my_proposals(status=status): |
56 | + self.outf.write('%s\n' % mp.url) |
57 | |
58 | === modified file 'breezy/plugins/propose/github.py' |
59 | --- breezy/plugins/propose/github.py 2019-01-01 22:31:13 +0000 |
60 | +++ breezy/plugins/propose/github.py 2019-01-08 21:33:41 +0000 |
61 | @@ -25,7 +25,6 @@ |
62 | MergeProposal, |
63 | MergeProposalBuilder, |
64 | MergeProposalExists, |
65 | - NoMergeProposal, |
66 | PrerequisiteBranchUnsupported, |
67 | UnsupportedHoster, |
68 | ) |
69 | @@ -105,6 +104,15 @@ |
70 | def url(self): |
71 | return self._pr.html_url |
72 | |
73 | + def _branch_from_part(self, part): |
74 | + return github_url_to_bzr_url(part.repo.html_url, part.ref) |
75 | + |
76 | + def get_source_branch_url(self): |
77 | + return self._branch_from_part(self._pr.head) |
78 | + |
79 | + def get_target_branch_url(self): |
80 | + return self._branch_from_part(self._pr.base) |
81 | + |
82 | def get_description(self): |
83 | return self._pr.body |
84 | |
85 | @@ -136,6 +144,8 @@ |
86 | |
87 | class GitHub(Hoster): |
88 | |
89 | + name = 'github' |
90 | + |
91 | supports_merge_proposal_labels = True |
92 | |
93 | def __repr__(self): |
94 | @@ -205,20 +215,30 @@ |
95 | def get_proposer(self, source_branch, target_branch): |
96 | return GitHubMergeProposalBuilder(self.gh, source_branch, target_branch) |
97 | |
98 | - def get_proposal(self, source_branch, target_branch): |
99 | + def iter_proposals(self, source_branch, target_branch, status='open'): |
100 | (source_owner, source_repo_name, source_branch_name) = ( |
101 | parse_github_url(source_branch)) |
102 | (target_owner, target_repo_name, target_branch_name) = ( |
103 | parse_github_url(target_branch)) |
104 | - target_repo = self.gh.get_repo("%s/%s" % (target_owner, target_repo_name)) |
105 | - for pull in target_repo.get_pulls(head=target_branch_name): |
106 | + target_repo = self.gh.get_repo( |
107 | + "%s/%s" % (target_owner, target_repo_name)) |
108 | + state = { |
109 | + 'open': 'open', |
110 | + 'merged': 'closed', |
111 | + 'closed': 'closed', |
112 | + 'all': 'all'} |
113 | + for pull in target_repo.get_pulls( |
114 | + head=target_branch_name, |
115 | + state=state[status]): |
116 | + if (status == 'closed' and pull.merged or |
117 | + status == 'merged' and not pull.merged): |
118 | + continue |
119 | if pull.head.ref != source_branch_name: |
120 | continue |
121 | if (pull.head.repo.owner.login != source_owner or |
122 | pull.head.repo.name != source_repo_name): |
123 | continue |
124 | - return GitHubMergeProposal(pull) |
125 | - raise NoMergeProposal() |
126 | + yield GitHubMergeProposal(pull) |
127 | |
128 | def hosts(self, branch): |
129 | try: |
130 | @@ -236,6 +256,24 @@ |
131 | raise UnsupportedHoster(branch) |
132 | return cls() |
133 | |
134 | + @classmethod |
135 | + def iter_instances(cls): |
136 | + yield cls() |
137 | + |
138 | + def iter_my_proposals(self, status='open'): |
139 | + query = ['is:pr'] |
140 | + if status == 'open': |
141 | + query.append('is:open') |
142 | + elif status == 'closed': |
143 | + # Note that we don't use is:closed here, since that also includes |
144 | + # merged pull requests. |
145 | + query.append('is:unmerged') |
146 | + elif status == 'merged': |
147 | + query.append('is:merged') |
148 | + query.append('author:%s' % self.gh.get_user().login) |
149 | + for issue in self.gh.search_issues(query=' '.join(query)): |
150 | + yield GitHubMergeProposal(issue.as_pull_request()) |
151 | + |
152 | |
153 | class GitHubMergeProposalBuilder(MergeProposalBuilder): |
154 | |
155 | |
156 | === modified file 'breezy/plugins/propose/gitlabs.py' |
157 | --- breezy/plugins/propose/gitlabs.py 2019-01-01 22:31:13 +0000 |
158 | +++ breezy/plugins/propose/gitlabs.py 2019-01-08 21:33:41 +0000 |
159 | @@ -33,12 +33,18 @@ |
160 | MergeProposal, |
161 | MergeProposalBuilder, |
162 | MergeProposalExists, |
163 | - NoMergeProposal, |
164 | NoSuchProject, |
165 | PrerequisiteBranchUnsupported, |
166 | UnsupportedHoster, |
167 | ) |
168 | |
169 | +def mp_status_to_status(status): |
170 | + return { |
171 | + 'all': 'all', |
172 | + 'open': 'opened', |
173 | + 'merged': 'merged', |
174 | + 'closed': 'closed'}[status] |
175 | + |
176 | |
177 | class NotGitLabUrl(errors.BzrError): |
178 | |
179 | @@ -83,26 +89,30 @@ |
180 | config.write(f) |
181 | |
182 | |
183 | +def iter_tokens(): |
184 | + import configparser |
185 | + from gitlab.config import _DEFAULT_FILES |
186 | + config = configparser.ConfigParser() |
187 | + config.read(_DEFAULT_FILES + [default_config_path()]) |
188 | + for name, section in config.items(): |
189 | + yield name, section |
190 | + |
191 | + |
192 | def connect_gitlab(host): |
193 | - from gitlab import Gitlab |
194 | + from gitlab import Gitlab, GitlabGetError |
195 | auth = AuthenticationConfig() |
196 | |
197 | url = 'https://%s' % host |
198 | credentials = auth.get_credentials('https', host) |
199 | if credentials is None: |
200 | - import gitlab |
201 | - import configparser |
202 | - from gitlab.config import _DEFAULT_FILES |
203 | - config = configparser.ConfigParser() |
204 | - config.read(_DEFAULT_FILES + [default_config_path()]) |
205 | - for name, section in config.items(): |
206 | + for name, section in iter_tokens(): |
207 | if section.get('url') == url: |
208 | credentials = section |
209 | break |
210 | else: |
211 | try: |
212 | return Gitlab(url) |
213 | - except gitlab.GitlabGetError: |
214 | + except GitlabGetError: |
215 | raise GitLabLoginMissing() |
216 | else: |
217 | credentials['url'] = url |
218 | @@ -138,8 +148,20 @@ |
219 | def set_description(self, description): |
220 | self._mr.description = description |
221 | |
222 | + def _branch_url_from_project(self, project_id, branch_name): |
223 | + project = self._mr.manager.gitlab.projects.get(project_id) |
224 | + return gitlab_url_to_bzr_url(project.http_url_to_repo, branch_name) |
225 | + |
226 | + def get_source_branch_url(self): |
227 | + return self._branch_url_from_project( |
228 | + self._mr.source_project_id, self._mr.source_branch) |
229 | + |
230 | + def get_target_branch_url(self): |
231 | + return self._branch_url_from_project( |
232 | + self._mr.target_project_id, self._mr.target_branch) |
233 | + |
234 | def is_merged(self): |
235 | - return (self._mr.attributes['state'] == 'merged') |
236 | + return (self._mr.state == 'merged') |
237 | |
238 | |
239 | def gitlab_url_to_bzr_url(url, name): |
240 | @@ -164,7 +186,7 @@ |
241 | (host, project_name, branch_name) = parse_gitlab_url(branch) |
242 | project = self.gl.projects.get(project_name) |
243 | return gitlab_url_to_bzr_url( |
244 | - project.attributes['ssh_url_to_repo'], branch_name) |
245 | + project.ssh_url_to_repo, branch_name) |
246 | |
247 | def publish_derived(self, local_branch, base_branch, name, project=None, |
248 | owner=None, revision_id=None, overwrite=False, |
249 | @@ -190,7 +212,7 @@ |
250 | target_project = base_project.forks.create({}) |
251 | else: |
252 | raise |
253 | - remote_repo_url = git_url_to_bzr_url(target_project.attributes['ssh_url_to_repo']) |
254 | + remote_repo_url = git_url_to_bzr_url(target_project.ssh_url_to_repo) |
255 | remote_dir = controldir.ControlDir.open(remote_repo_url) |
256 | try: |
257 | push_result = remote_dir.push_branch( |
258 | @@ -203,7 +225,7 @@ |
259 | local_branch, revision_id=revision_id, overwrite=overwrite, |
260 | name=name, lossy=True) |
261 | public_url = gitlab_url_to_bzr_url( |
262 | - target_project.attributes['http_url_to_repo'], name) |
263 | + target_project.http_url_to_repo, name) |
264 | return push_result.target_branch, public_url |
265 | |
266 | def get_derived_branch(self, base_branch, name, project=None, owner=None): |
267 | @@ -228,12 +250,13 @@ |
268 | raise errors.NotBranchError('%s/%s/%s' % (self.gl.url, owner, project)) |
269 | raise |
270 | return _mod_branch.Branch.open(gitlab_url_to_bzr_url( |
271 | - target_project.attributes['ssh_url_to_repo'], name)) |
272 | + target_project.ssh_url_to_repo, name)) |
273 | |
274 | def get_proposer(self, source_branch, target_branch): |
275 | return GitlabMergeProposalBuilder(self.gl, source_branch, target_branch) |
276 | |
277 | - def get_proposal(self, source_branch, target_branch): |
278 | + def iter_proposals(self, source_branch, target_branch, status): |
279 | + import gitlab |
280 | (source_host, source_project_name, source_branch_name) = ( |
281 | parse_gitlab_url(source_branch)) |
282 | (target_host, target_project_name, target_branch_name) = ( |
283 | @@ -243,19 +266,18 @@ |
284 | self.gl.auth() |
285 | source_project = self.gl.projects.get(source_project_name) |
286 | target_project = self.gl.projects.get(target_project_name) |
287 | + state = mp_status_to_status(status) |
288 | try: |
289 | - for mr in target_project.mergerequests.list(state='all'): |
290 | - attrs = mr.attributes |
291 | - if (attrs['source_project_id'] != source_project.id or |
292 | - attrs['source_branch'] != source_branch_name or |
293 | - attrs['target_project_id'] != target_project.id or |
294 | - attrs['target_branch'] != target_branch_name): |
295 | + for mr in target_project.mergerequests.list(state=state): |
296 | + if (mr.source_project_id != source_project.id or |
297 | + mr.source_branch != source_branch_name or |
298 | + mr.target_project_id != target_project.id or |
299 | + mr.target_branch != target_branch_name): |
300 | continue |
301 | - return GitLabMergeProposal(mr) |
302 | + yield GitLabMergeProposal(mr) |
303 | except gitlab.GitlabListError as e: |
304 | if e.response_code == 403: |
305 | - raise PermissionDenied(e.error_message) |
306 | - raise NoMergeProposal() |
307 | + raise errors.PermissionDenied(e.error_message) |
308 | |
309 | def hosts(self, branch): |
310 | try: |
311 | @@ -287,6 +309,22 @@ |
312 | raise |
313 | return cls(gl) |
314 | |
315 | + @classmethod |
316 | + def iter_instances(cls): |
317 | + from gitlab import Gitlab |
318 | + for name, credentials in iter_tokens(): |
319 | + if 'url' not in credentials: |
320 | + continue |
321 | + gl = Gitlab(**credentials) |
322 | + yield cls(gl) |
323 | + |
324 | + def iter_my_proposals(self, status='open'): |
325 | + state = mp_status_to_status(status) |
326 | + self.gl.auth() |
327 | + for mp in self.gl.mergerequests.list( |
328 | + owner=self.gl.user.username, state=state): |
329 | + yield GitLabMergeProposal(mp) |
330 | + |
331 | |
332 | class GitlabMergeProposalBuilder(MergeProposalBuilder): |
333 | |
334 | @@ -343,8 +381,23 @@ |
335 | merge_request = source_project.mergerequests.create(kwargs) |
336 | except gitlab.GitlabCreateError as e: |
337 | if e.response_code == 403: |
338 | - raise PermissionDenied(e.error_message) |
339 | + raise errors.PermissionDenied(e.error_message) |
340 | if e.response_code == 409: |
341 | raise MergeProposalExists(self.source_branch.user_url) |
342 | raise |
343 | return GitLabMergeProposal(merge_request) |
344 | + |
345 | + |
346 | +def register_gitlab_instance(shortname, url): |
347 | + """Register a gitlab instance. |
348 | + |
349 | + :param shortname: Short name (e.g. "gitlab") |
350 | + :param url: URL to the gitlab instance |
351 | + """ |
352 | + from breezy.bugtracker import ( |
353 | + tracker_registry, |
354 | + ProjectIntegerBugTracker, |
355 | + ) |
356 | + tracker_registry.register( |
357 | + shortname, ProjectIntegerBugTracker( |
358 | + shortname, url + '/{project}/issues/{id}')) |
359 | |
360 | === modified file 'breezy/plugins/propose/launchpad.py' |
361 | --- breezy/plugins/propose/launchpad.py 2019-01-01 22:31:13 +0000 |
362 | +++ breezy/plugins/propose/launchpad.py 2019-01-08 21:33:41 +0000 |
363 | @@ -27,7 +27,6 @@ |
364 | MergeProposal, |
365 | MergeProposalBuilder, |
366 | MergeProposalExists, |
367 | - NoMergeProposal, |
368 | UnsupportedHoster, |
369 | ) |
370 | |
371 | @@ -38,6 +37,7 @@ |
372 | hooks, |
373 | urlutils, |
374 | ) |
375 | +from ...git.refs import ref_to_branch_name |
376 | from ...lazy_import import lazy_import |
377 | lazy_import(globals(), """ |
378 | from breezy.plugins.launchpad import ( |
379 | @@ -50,16 +50,20 @@ |
380 | |
381 | # TODO(jelmer): Make selection of launchpad staging a configuration option. |
382 | |
383 | -MERGE_PROPOSAL_STATUSES = [ |
384 | - 'Work in progress', |
385 | - 'Needs review', |
386 | - 'Approved', |
387 | - 'Rejected', |
388 | - 'Merged', |
389 | - 'Code failed to merge', |
390 | - 'Queued', |
391 | - 'Superseded', |
392 | - ] |
393 | +def status_to_lp_mp_statuses(status): |
394 | + statuses = [] |
395 | + if status in ('open', 'all'): |
396 | + statuses.extend([ |
397 | + 'Work in progress', |
398 | + 'Needs review', |
399 | + 'Approved', |
400 | + 'Code failed to merge', |
401 | + 'Queued']) |
402 | + if status in ('closed', 'all'): |
403 | + statuses.extend(['Rejected', 'Superseded']) |
404 | + if status in ('merged', 'all'): |
405 | + statuses.append('Merged') |
406 | + return statuses |
407 | |
408 | |
409 | def plausible_launchpad_url(url): |
410 | @@ -103,6 +107,26 @@ |
411 | def __init__(self, mp): |
412 | self._mp = mp |
413 | |
414 | + def get_source_branch_url(self): |
415 | + if self._mp.source_branch: |
416 | + return self._mp.source_branch.bzr_identity |
417 | + else: |
418 | + branch_name = ref_to_branch_name( |
419 | + self._mp.source_git_path.encode('utf-8')) |
420 | + return urlutils.join_segment_parameters( |
421 | + self._mp.source_git_repository.git_identity, |
422 | + {"branch": branch_name}) |
423 | + |
424 | + def get_target_branch_url(self): |
425 | + if self._mp.target_branch: |
426 | + return self._mp.target_branch.bzr_identity |
427 | + else: |
428 | + branch_name = ref_to_branch_name( |
429 | + self._mp.target_git_path.encode('utf-8')) |
430 | + return urlutils.join_segment_parameters( |
431 | + self._mp.target_git_repository.git_identity, |
432 | + {"branch": branch_name}) |
433 | + |
434 | @property |
435 | def url(self): |
436 | return lp_api.canonical_url(self._mp) |
437 | @@ -114,6 +138,8 @@ |
438 | class Launchpad(Hoster): |
439 | """The Launchpad hosting service.""" |
440 | |
441 | + name = 'launchpad' |
442 | + |
443 | # https://bugs.launchpad.net/launchpad/+bug/397676 |
444 | supports_merge_proposal_labels = False |
445 | |
446 | @@ -142,7 +168,8 @@ |
447 | url, params = urlutils.split_segment_parameters(branch.user_url) |
448 | (scheme, user, password, host, port, path) = urlutils.parse_url( |
449 | url) |
450 | - repo_lp = self.launchpad.git_repositories.getByPath(path=path.strip('/')) |
451 | + repo_lp = self.launchpad.git_repositories.getByPath( |
452 | + path=path.strip('/')) |
453 | try: |
454 | ref_path = params['ref'] |
455 | except KeyError: |
456 | @@ -155,13 +182,15 @@ |
457 | return (repo_lp, ref_lp) |
458 | |
459 | def _get_lp_bzr_branch_from_branch(self, branch): |
460 | - return self.launchpad.branches.getByUrl(url=urlutils.unescape(branch.user_url)) |
461 | + return self.launchpad.branches.getByUrl( |
462 | + url=urlutils.unescape(branch.user_url)) |
463 | |
464 | def _get_derived_git_path(self, base_path, owner, project): |
465 | base_repo = self.launchpad.git_repositories.getByPath(path=base_path) |
466 | if project is None: |
467 | project = '/'.join(base_repo.unique_name.split('/')[1:]) |
468 | - # TODO(jelmer): Surely there is a better way of creating one of these URLs? |
469 | + # TODO(jelmer): Surely there is a better way of creating one of these |
470 | + # URLs? |
471 | return "~%s/%s" % (owner, project) |
472 | |
473 | def _publish_git(self, local_branch, base_path, name, owner, project=None, |
474 | @@ -177,27 +206,31 @@ |
475 | if dir_to is None: |
476 | try: |
477 | br_to = local_branch.create_clone_on_transport( |
478 | - to_transport, revision_id=revision_id, name=name, |
479 | - stacked_on=main_branch.user_url) |
480 | + to_transport, revision_id=revision_id, name=name) |
481 | except errors.NoRoundtrippingSupport: |
482 | br_to = local_branch.create_clone_on_transport( |
483 | to_transport, revision_id=revision_id, name=name, |
484 | - stacked_on=main_branch.user_url, lossy=True) |
485 | + lossy=True) |
486 | else: |
487 | try: |
488 | - dir_to = dir_to.push_branch(local_branch, revision_id, overwrite=overwrite, name=name) |
489 | + dir_to = dir_to.push_branch( |
490 | + local_branch, revision_id, overwrite=overwrite, name=name) |
491 | except errors.NoRoundtrippingSupport: |
492 | if not allow_lossy: |
493 | raise |
494 | - dir_to = dir_to.push_branch(local_branch, revision_id, overwrite=overwrite, name=name, lossy=True) |
495 | + dir_to = dir_to.push_branch( |
496 | + local_branch, revision_id, overwrite=overwrite, name=name, |
497 | + lossy=True) |
498 | br_to = dir_to.target_branch |
499 | - return br_to, ("https://git.launchpad.net/%s/+ref/%s" % (to_path, name)) |
500 | + return br_to, ( |
501 | + "https://git.launchpad.net/%s/+ref/%s" % (to_path, name)) |
502 | |
503 | def _get_derived_bzr_path(self, base_branch, name, owner, project): |
504 | if project is None: |
505 | base_branch_lp = self._get_lp_bzr_branch_from_branch(base_branch) |
506 | project = '/'.join(base_branch_lp.unique_name.split('/')[1:-1]) |
507 | - # TODO(jelmer): Surely there is a better way of creating one of these URLs? |
508 | + # TODO(jelmer): Surely there is a better way of creating one of these |
509 | + # URLs? |
510 | return "~%s/%s/%s" % (owner, project, name) |
511 | |
512 | def get_push_url(self, branch): |
513 | @@ -211,8 +244,9 @@ |
514 | else: |
515 | raise AssertionError |
516 | |
517 | - def _publish_bzr(self, local_branch, base_branch, name, owner, project=None, |
518 | - revision_id=None, overwrite=False, allow_lossy=True): |
519 | + def _publish_bzr(self, local_branch, base_branch, name, owner, |
520 | + project=None, revision_id=None, overwrite=False, |
521 | + allow_lossy=True): |
522 | to_path = self._get_derived_bzr_path(base_branch, name, owner, project) |
523 | to_transport = get_transport("lp:" + to_path) |
524 | try: |
525 | @@ -222,9 +256,11 @@ |
526 | dir_to = None |
527 | |
528 | if dir_to is None: |
529 | - br_to = local_branch.create_clone_on_transport(to_transport, revision_id=revision_id) |
530 | + br_to = local_branch.create_clone_on_transport( |
531 | + to_transport, revision_id=revision_id) |
532 | else: |
533 | - br_to = dir_to.push_branch(local_branch, revision_id, overwrite=overwrite).target_branch |
534 | + br_to = dir_to.push_branch( |
535 | + local_branch, revision_id, overwrite=overwrite).target_branch |
536 | return br_to, ("https://code.launchpad.net/" + to_path) |
537 | |
538 | def _split_url(self, url): |
539 | @@ -239,8 +275,9 @@ |
540 | raise ValueError("unknown host %s" % host) |
541 | return (vcs, user, password, path, params) |
542 | |
543 | - def publish_derived(self, local_branch, base_branch, name, project=None, owner=None, |
544 | - revision_id=None, overwrite=False, allow_lossy=True): |
545 | + def publish_derived(self, local_branch, base_branch, name, project=None, |
546 | + owner=None, revision_id=None, overwrite=False, |
547 | + allow_lossy=True): |
548 | """Publish a branch to the site, derived from base_branch. |
549 | |
550 | :param base_branch: branch to derive the new branch from |
551 | @@ -252,8 +289,8 @@ |
552 | """ |
553 | if owner is None: |
554 | owner = self.launchpad.me.name |
555 | - (base_vcs, base_user, base_password, base_path, base_params) = self._split_url( |
556 | - base_branch.user_url) |
557 | + (base_vcs, base_user, base_password, base_path, |
558 | + base_params) = self._split_url(base_branch.user_url) |
559 | # TODO(jelmer): Prevent publishing to development focus |
560 | if base_vcs == 'bzr': |
561 | return self._publish_bzr( |
562 | @@ -271,10 +308,11 @@ |
563 | def get_derived_branch(self, base_branch, name, project=None, owner=None): |
564 | if owner is None: |
565 | owner = self.launchpad.me.name |
566 | - (base_vcs, base_user, base_password, base_path, base_params) = self._split_url( |
567 | - base_branch.user_url) |
568 | + (base_vcs, base_user, base_password, base_path, |
569 | + base_params) = self._split_url(base_branch.user_url) |
570 | if base_vcs == 'bzr': |
571 | - to_path = self._get_derived_bzr_path(base_branch, name, owner, project) |
572 | + to_path = self._get_derived_bzr_path( |
573 | + base_branch, name, owner, project) |
574 | return _mod_branch.Branch.open("lp:" + to_path) |
575 | elif base_vcs == 'git': |
576 | to_path = self._get_derived_git_path( |
577 | @@ -284,42 +322,37 @@ |
578 | else: |
579 | raise AssertionError('not a valid Launchpad URL') |
580 | |
581 | - def get_proposal(self, source_branch, target_branch): |
582 | - (base_vcs, base_user, base_password, base_path, base_params) = ( |
583 | - self._split_url(target_branch.user_url)) |
584 | + def iter_proposals(self, source_branch, target_branch, status='open'): |
585 | + (base_vcs, base_user, base_password, base_path, |
586 | + base_params) = self._split_url(target_branch.user_url) |
587 | + statuses = status_to_lp_mp_statuses(status) |
588 | if base_vcs == 'bzr': |
589 | target_branch_lp = self.launchpad.branches.getByUrl( |
590 | url=target_branch.user_url) |
591 | source_branch_lp = self.launchpad.branches.getByUrl( |
592 | url=source_branch.user_url) |
593 | - for mp in target_branch_lp.getMergeProposals( |
594 | - status=MERGE_PROPOSAL_STATUSES): |
595 | - if mp.target_branch != target_branch_lp: |
596 | - continue |
597 | - if mp.source_branch != source_branch_lp: |
598 | - continue |
599 | - return LaunchpadMergeProposal(mp) |
600 | - raise NoMergeProposal() |
601 | + for mp in target_branch_lp.getMergeProposals(status=statuses): |
602 | + if mp.source_branch_link != source_branch_lp.self_link: |
603 | + continue |
604 | + yield LaunchpadMergeProposal(mp) |
605 | elif base_vcs == 'git': |
606 | (source_repo_lp, source_branch_lp) = ( |
607 | self.lp_host._get_lp_git_ref_from_branch(source_branch)) |
608 | (target_repo_lp, target_branch_lp) = ( |
609 | self.lp_host._get_lp_git_ref_from_branch(target_branch)) |
610 | - for mp in target_branch_lp.getMergeProposals( |
611 | - status=MERGE_PROPOSAL_STATUSES): |
612 | + for mp in target_branch_lp.getMergeProposals(status=statuses): |
613 | if (target_branch_lp.path != mp.target_git_path or |
614 | target_repo_lp != mp.target_git_repository or |
615 | source_branch_lp.path != mp.source_git_path or |
616 | source_repo_lp != mp.source_git_repository): |
617 | continue |
618 | - return LaunchpadMergeProposal(mp) |
619 | - raise NoMergeProposal() |
620 | + yield LaunchpadMergeProposal(mp) |
621 | else: |
622 | raise AssertionError('not a valid Launchpad URL') |
623 | |
624 | def get_proposer(self, source_branch, target_branch): |
625 | - (base_vcs, base_user, base_password, base_path, base_params) = ( |
626 | - self._split_url(target_branch.user_url)) |
627 | + (base_vcs, base_user, base_password, base_path, |
628 | + base_params) = self._split_url(target_branch.user_url) |
629 | if base_vcs == 'bzr': |
630 | return LaunchpadBazaarMergeProposalBuilder( |
631 | self, source_branch, target_branch) |
632 | @@ -329,6 +362,15 @@ |
633 | else: |
634 | raise AssertionError('not a valid Launchpad URL') |
635 | |
636 | + @classmethod |
637 | + def iter_instances(cls): |
638 | + yield cls() |
639 | + |
640 | + def iter_my_proposals(self, status='open'): |
641 | + statuses = status_to_lp_mp_statuses(status) |
642 | + for mp in self.launchpad.me.getMergeProposals(status=statuses): |
643 | + yield LaunchpadMergeProposal(mp) |
644 | + |
645 | |
646 | def connect_launchpad(lp_instance='production'): |
647 | service = lp_registration.LaunchpadService(lp_instance=lp_instance) |
648 | @@ -354,13 +396,16 @@ |
649 | self.lp_host = lp_host |
650 | self.launchpad = lp_host.launchpad |
651 | self.source_branch = source_branch |
652 | - self.source_branch_lp = self.launchpad.branches.getByUrl(url=source_branch.user_url) |
653 | + self.source_branch_lp = self.launchpad.branches.getByUrl( |
654 | + url=source_branch.user_url) |
655 | if target_branch is None: |
656 | self.target_branch_lp = self.source_branch_lp.get_target() |
657 | - self.target_branch = _mod_branch.Branch.open(self.target_branch_lp.bzr_identity) |
658 | + self.target_branch = _mod_branch.Branch.open( |
659 | + self.target_branch_lp.bzr_identity) |
660 | else: |
661 | self.target_branch = target_branch |
662 | - self.target_branch_lp = self.launchpad.branches.getByUrl(url=target_branch.user_url) |
663 | + self.target_branch_lp = self.launchpad.branches.getByUrl( |
664 | + url=target_branch.user_url) |
665 | self.commit_message = message |
666 | self.approve = approve |
667 | self.fixes = fixes |
668 | @@ -414,7 +459,7 @@ |
669 | _call_webservice( |
670 | mp.createComment, |
671 | vote=u'Approve', |
672 | - subject='', # Use the default subject. |
673 | + subject='', # Use the default subject. |
674 | content=u"Rubberstamp! Proposer approves of own proposal.") |
675 | _call_webservice(mp.setStatus, status=u'Approved', |
676 | revid=self.source_branch.last_revision()) |
677 | @@ -478,13 +523,17 @@ |
678 | self.lp_host = lp_host |
679 | self.launchpad = lp_host.launchpad |
680 | self.source_branch = source_branch |
681 | - (self.source_repo_lp, self.source_branch_lp) = self.lp_host._get_lp_git_ref_from_branch(source_branch) |
682 | + (self.source_repo_lp, |
683 | + self.source_branch_lp) = self.lp_host._get_lp_git_ref_from_branch( |
684 | + source_branch) |
685 | if target_branch is None: |
686 | self.target_branch_lp = self.source_branch.get_target() |
687 | - self.target_branch = _mod_branch.Branch.open(self.target_branch_lp.git_https_url) |
688 | + self.target_branch = _mod_branch.Branch.open( |
689 | + self.target_branch_lp.git_https_url) |
690 | else: |
691 | self.target_branch = target_branch |
692 | - (self.target_repo_lp, self.target_branch_lp) = self.lp_host._get_lp_git_ref_from_branch(target_branch) |
693 | + (self.target_repo_lp, self.target_branch_lp) = ( |
694 | + self.lp_host._get_lp_git_ref_from_branch(target_branch)) |
695 | self.commit_message = message |
696 | self.approve = approve |
697 | self.fixes = fixes |
698 | @@ -538,7 +587,7 @@ |
699 | _call_webservice( |
700 | mp.createComment, |
701 | vote=u'Approve', |
702 | - subject='', # Use the default subject. |
703 | + subject='', # Use the default subject. |
704 | content=u"Rubberstamp! Proposer approves of own proposal.") |
705 | _call_webservice( |
706 | mp.setStatus, status=u'Approved', |
707 | @@ -550,7 +599,8 @@ |
708 | if labels: |
709 | raise LabelsUnsupported() |
710 | if prerequisite_branch is not None: |
711 | - (prereq_repo_lp, prereq_branch_lp) = self.lp_host._get_lp_git_ref_from_branch(prerequisite_branch) |
712 | + (prereq_repo_lp, prereq_branch_lp) = ( |
713 | + self.lp_host._get_lp_git_ref_from_branch(prerequisite_branch)) |
714 | else: |
715 | prereq_branch_lp = None |
716 | if reviewers is None: |
717 | |
718 | === modified file 'breezy/plugins/propose/propose.py' |
719 | --- breezy/plugins/propose/propose.py 2019-01-01 22:31:13 +0000 |
720 | +++ breezy/plugins/propose/propose.py 2019-01-08 21:33:41 +0000 |
721 | @@ -43,14 +43,6 @@ |
722 | self.url = url |
723 | |
724 | |
725 | -class NoMergeProposal(errors.BzrError): |
726 | - |
727 | - _fmt = "No merge proposal exists." |
728 | - |
729 | - def __init__(self): |
730 | - errors.BzrError.__init__(self) |
731 | - |
732 | - |
733 | class UnsupportedHoster(errors.BzrError): |
734 | |
735 | _fmt = "No supported hoster for %(branch)s." |
736 | @@ -108,6 +100,14 @@ |
737 | """Set the description of the merge proposal.""" |
738 | raise NotImplementedError(self.set_description) |
739 | |
740 | + def get_source_branch_url(self): |
741 | + """Return the source branch.""" |
742 | + raise NotImplementedError(self.get_source_branch_url) |
743 | + |
744 | + def get_target_branch_url(self): |
745 | + """Return the target branch.""" |
746 | + raise NotImplementedError(self.get_target_branch_url) |
747 | + |
748 | def close(self): |
749 | """Close the merge proposal (without merging it).""" |
750 | raise NotImplementedError(self.close) |
751 | @@ -192,15 +192,15 @@ |
752 | """ |
753 | raise NotImplementedError(self.get_proposer) |
754 | |
755 | - def get_proposal(self, source_branch, target_branch): |
756 | + def iter_proposals(self, source_branch, target_branch, status='open'): |
757 | """Get a merge proposal for a specified branch tuple. |
758 | |
759 | :param source_branch: Source branch |
760 | :param target_branch: Target branch |
761 | - :raise NoMergeProposal: if no merge proposal can be found |
762 | - :return: A MergeProposal object |
763 | + :param status: Status of proposals to iterate over |
764 | + :return: Iterate over MergeProposal object |
765 | """ |
766 | - raise NotImplementedError(self.get_proposal) |
767 | + raise NotImplementedError(self.iter_proposals) |
768 | |
769 | def hosts(self, branch): |
770 | """Return true if this hoster hosts given branch.""" |
771 | @@ -212,7 +212,23 @@ |
772 | raise NotImplementedError(cls.probe) |
773 | |
774 | # TODO(jelmer): Some way of cleaning up old branch proposals/branches |
775 | - # TODO(jelmer): Some way of checking up on outstanding merge proposals |
776 | + |
777 | + def iter_my_proposals(self, status='open'): |
778 | + """Iterate over the proposals created by the currently logged in user. |
779 | + |
780 | + :param status: Only yield proposals with this status |
781 | + (one of: 'open', 'closed', 'merged', 'all') |
782 | + :return: Iterator over MergeProposal objects |
783 | + """ |
784 | + raise NotImplementedError(self.iter_my_proposals) |
785 | + |
786 | + @classmethod |
787 | + def iter_instances(cls): |
788 | + """Iterate instances. |
789 | + |
790 | + :return: Iterator over Hoster instances |
791 | + """ |
792 | + raise NotImplementedError(cls.iter_instances) |
793 | |
794 | |
795 | def get_hoster(branch, possible_hosters=None): |
Thanks! Changes look good, one teeny nit inline.