Merge lp:~cjwatson/launchpad/git-mp-basic-browser into lp:launchpad
- git-mp-basic-browser
- Merge into devel
Proposed by
Colin Watson
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 17463 | ||||
Proposed branch: | lp:~cjwatson/launchpad/git-mp-basic-browser | ||||
Merge into: | lp:launchpad | ||||
Prerequisite: | lp:~cjwatson/launchpad/storm-vocabulary-base | ||||
Diff against target: |
1026 lines (+562/-85) 13 files modified
lib/lp/code/browser/branchmergeproposal.py (+95/-27) lib/lp/code/browser/configure.zcml (+3/-0) lib/lp/code/browser/gitref.py (+61/-0) lib/lp/code/browser/tests/test_branchmergeproposal.py (+208/-54) lib/lp/code/errors.py (+9/-2) lib/lp/code/interfaces/gitrepository.py (+34/-0) lib/lp/code/model/gitref.py (+3/-0) lib/lp/code/model/gitrepository.py (+22/-0) lib/lp/code/model/tests/test_gitrepository.py (+74/-0) lib/lp/code/templates/branchmergeproposal-resubmit.pt (+1/-1) lib/lp/code/templates/gitref-index.pt (+7/-0) lib/lp/code/templates/gitref-pending-merges.pt (+43/-0) lib/lp/testing/factory.py (+2/-1) |
||||
To merge this branch: | bzr merge lp:~cjwatson/launchpad/git-mp-basic-browser | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant (community) | code | Approve | |
Review via email: mp+257674@code.launchpad.net |
This proposal supersedes a proposal from 2015-04-28.
Commit message
Add Git support to most of the merge proposal views.
Description of the change
Add Git support to most of the merge proposal views.
GitRef:
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 'lib/lp/code/browser/branchmergeproposal.py' |
2 | --- lib/lp/code/browser/branchmergeproposal.py 2015-04-22 16:41:41 +0000 |
3 | +++ lib/lp/code/browser/branchmergeproposal.py 2015-04-29 12:39:54 +0000 |
4 | @@ -86,6 +86,7 @@ |
5 | InvalidBranchMergeProposal, |
6 | WrongBranchMergeProposal, |
7 | ) |
8 | +from lp.code.interfaces.branch import IBranch |
9 | from lp.code.interfaces.branchmergeproposal import IBranchMergeProposal |
10 | from lp.code.interfaces.codereviewcomment import ICodeReviewComment |
11 | from lp.code.interfaces.codereviewinlinecomment import ( |
12 | @@ -355,7 +356,7 @@ |
13 | @property |
14 | def codebrowse_url(self): |
15 | """Return the link to codebrowse for this branch.""" |
16 | - return self.context.source_branch.getCodebrowseUrl() |
17 | + return self.context.merge_source.getCodebrowseUrl() |
18 | |
19 | |
20 | class BranchMergeProposalRevisionIdMixin: |
21 | @@ -373,6 +374,9 @@ |
22 | return None |
23 | # If the source branch is REMOTE, then there won't be any ids. |
24 | source_branch = self.context.source_branch |
25 | + if source_branch is None: |
26 | + # Git doesn't have revision numbers. Just use the ids. |
27 | + return revision_id |
28 | if source_branch.branch_type == BranchType.REMOTE: |
29 | return revision_id |
30 | else: |
31 | @@ -586,12 +590,18 @@ |
32 | def initialize(self): |
33 | super(BranchMergeProposalView, self).initialize() |
34 | cache = IJSONRequestCache(self.request) |
35 | - cache.objects.update({ |
36 | - 'branch_diff_link': |
37 | - 'https://%s/+loggerhead/%s/diff/' % ( |
38 | - config.launchpad.code_domain, |
39 | - self.context.source_branch.unique_name), |
40 | - }) |
41 | + if IBranch.providedBy(self.context.merge_source): |
42 | + cache.objects.update({ |
43 | + 'branch_diff_link': |
44 | + 'https://%s/+loggerhead/%s/diff/' % ( |
45 | + config.launchpad.code_domain, |
46 | + self.context.source_branch.unique_name), |
47 | + }) |
48 | + else: |
49 | + # XXX cjwatson 2015-04-29: Unimplemented for Git; this would |
50 | + # require something in the webapp which proxies diff requests to |
51 | + # the code browser, or an integrated code browser. |
52 | + pass |
53 | if getFeatureFlag("longpoll.merge_proposals.enabled"): |
54 | cache.objects['merge_proposal_event_key'] = subscribe( |
55 | self.context).event_key |
56 | @@ -618,7 +628,9 @@ |
57 | # Sort the comments by date order. |
58 | merge_proposal = self.context |
59 | groups = list(merge_proposal.getRevisionsSinceReviewStart()) |
60 | - source = DecoratedBranch(merge_proposal.source_branch) |
61 | + source = merge_proposal.merge_source |
62 | + if IBranch.providedBy(source): |
63 | + source = DecoratedBranch(source) |
64 | comments = [] |
65 | if getFeatureFlag('code.incremental_diffs.enabled'): |
66 | ranges = [ |
67 | @@ -685,6 +697,8 @@ |
68 | |
69 | @property |
70 | def pending_diff(self): |
71 | + if self.context.source_branch is None: |
72 | + return False |
73 | return ( |
74 | self.context.next_preview_diff_job is not None or |
75 | self.context.source_branch.pending_writes) |
76 | @@ -705,6 +719,10 @@ |
77 | |
78 | @property |
79 | def spec_links(self): |
80 | + if self.context.source_branch is None: |
81 | + # XXX cjwatson 2015-04-16: Implement once Git refs have linked |
82 | + # blueprints. |
83 | + return [] |
84 | return list( |
85 | self.context.source_branch.getSpecificationLinks(self.user)) |
86 | |
87 | @@ -748,12 +766,16 @@ |
88 | @property |
89 | def status_config(self): |
90 | """The config to configure the ChoiceSource JS widget.""" |
91 | + if IBranch.providedBy(self.context.merge_source): |
92 | + source_revid = self.context.merge_source.last_scanned_id |
93 | + else: |
94 | + source_revid = self.context.merge_source.commit_sha1 |
95 | return simplejson.dumps({ |
96 | 'status_widget_items': vocabulary_to_choice_edit_items( |
97 | self._createStatusVocabulary(), |
98 | css_class_prefix='mergestatus'), |
99 | 'status_value': self.context.queue_status.title, |
100 | - 'source_revid': self.context.source_branch.last_scanned_id, |
101 | + 'source_revid': source_revid, |
102 | 'user_can_edit_status': check_permission( |
103 | 'launchpad.Edit', self.context), |
104 | }) |
105 | @@ -965,19 +987,35 @@ |
106 | schema = ResubmitSchema |
107 | for_input = True |
108 | page_title = label = "Resubmit proposal to merge" |
109 | - field_names = [ |
110 | - 'source_branch', |
111 | - 'target_branch', |
112 | - 'prerequisite_branch', |
113 | - 'description', |
114 | - 'break_link', |
115 | - ] |
116 | |
117 | def initialize(self): |
118 | self.cancel_url = canonical_url(self.context) |
119 | super(BranchMergeProposalResubmitView, self).initialize() |
120 | |
121 | @property |
122 | + def field_names(self): |
123 | + if IBranch.providedBy(self.context.merge_source): |
124 | + field_names = [ |
125 | + 'source_branch', |
126 | + 'target_branch', |
127 | + 'prerequisite_branch', |
128 | + ] |
129 | + else: |
130 | + field_names = [ |
131 | + 'source_git_repository', |
132 | + 'source_git_path', |
133 | + 'target_git_repository', |
134 | + 'target_git_path', |
135 | + 'prerequisite_git_repository', |
136 | + 'prerequisite_git_path', |
137 | + ] |
138 | + field_names.extend([ |
139 | + 'description', |
140 | + 'break_link', |
141 | + ]) |
142 | + return field_names |
143 | + |
144 | + @property |
145 | def initial_values(self): |
146 | UNSET = object() |
147 | items = ((key, getattr(self.context, key, UNSET)) for key in |
148 | @@ -988,10 +1026,24 @@ |
149 | def resubmit_action(self, action, data): |
150 | """Resubmit this proposal.""" |
151 | try: |
152 | + if IBranch.providedBy(self.context.merge_source): |
153 | + merge_source = data['source_branch'] |
154 | + merge_target = data['target_branch'] |
155 | + merge_prerequisite = data['prerequisite_branch'] |
156 | + else: |
157 | + merge_source = data['source_git_repository'].getRefByPath( |
158 | + data['source_git_path']) |
159 | + merge_target = data['target_git_repository'].getRefByPath( |
160 | + data['target_git_path']) |
161 | + if data['prerequisite_git_repository']: |
162 | + merge_prerequisite = ( |
163 | + data['prerequisite_git_repository'].getRefByPath( |
164 | + data['prerequisite_git_path'])) |
165 | + else: |
166 | + merge_prerequisite = None |
167 | proposal = self.context.resubmit( |
168 | - self.user, data['source_branch'], data['target_branch'], |
169 | - data['prerequisite_branch'], data['description'], |
170 | - data['break_link']) |
171 | + self.user, merge_source, merge_target, merge_prerequisite, |
172 | + data['description'], data['break_link']) |
173 | except BranchMergeProposalExists as e: |
174 | message = structured( |
175 | 'Cannot resubmit because <a href="%(url)s">a similar merge' |
176 | @@ -1072,18 +1124,31 @@ |
177 | """The view to mark a merge proposal as merged.""" |
178 | schema = IBranchMergeProposal |
179 | page_title = label = "Edit branch merge proposal" |
180 | - field_names = ["merged_revno"] |
181 | for_input = True |
182 | |
183 | @property |
184 | + def field_names(self): |
185 | + if IBranch.providedBy(self.context.merge_target): |
186 | + return ["merged_revno"] |
187 | + else: |
188 | + return ["merged_revision_id"] |
189 | + |
190 | + @property |
191 | def initial_values(self): |
192 | # Default to the tip of the target branch, on the assumption that the |
193 | # source branch has just been merged into it. |
194 | - if self.context.merged_revno is not None: |
195 | - revno = self.context.merged_revno |
196 | + if IBranch.providedBy(self.context.merge_target): |
197 | + if self.context.merged_revno is not None: |
198 | + revno = self.context.merged_revno |
199 | + else: |
200 | + revno = self.context.merge_target.revision_count |
201 | + return {'merged_revno': revno} |
202 | else: |
203 | - revno = self.context.target_branch.revision_count |
204 | - return {'merged_revno': revno} |
205 | + if self.context.merged_revision_id is not None: |
206 | + revision_id = self.context.merged_revision_id |
207 | + else: |
208 | + revision_id = self.context.merge_target.commit_sha1 |
209 | + return {'merged_revision_id': revision_id} |
210 | |
211 | @property |
212 | def next_url(self): |
213 | @@ -1095,13 +1160,16 @@ |
214 | @notify |
215 | def mark_merged_action(self, action, data): |
216 | """Update the whiteboard and go back to the source branch.""" |
217 | - revno = data['merged_revno'] |
218 | + if IBranch.providedBy(self.context.merge_target): |
219 | + kwargs = {'merged_revno': data['merged_revno']} |
220 | + else: |
221 | + kwargs = {'merged_revision_id': data['merged_revision_id']} |
222 | if self.context.queue_status == BranchMergeProposalStatus.MERGED: |
223 | - self.context.markAsMerged(merged_revno=revno) |
224 | + self.context.markAsMerged(**kwargs) |
225 | self.request.response.addNotification( |
226 | 'The proposal\'s merged revision has been updated.') |
227 | else: |
228 | - self.context.markAsMerged(revno, merge_reporter=self.user) |
229 | + self.context.markAsMerged(merge_reporter=self.user, **kwargs) |
230 | self.request.response.addNotification( |
231 | 'The proposal has now been marked as merged.') |
232 | |
233 | |
234 | === modified file 'lib/lp/code/browser/configure.zcml' |
235 | --- lib/lp/code/browser/configure.zcml 2015-04-22 16:11:40 +0000 |
236 | +++ lib/lp/code/browser/configure.zcml 2015-04-29 12:39:54 +0000 |
237 | @@ -824,6 +824,9 @@ |
238 | <browser:page |
239 | name="++ref-commits" |
240 | template="../templates/gitref-commits.pt"/> |
241 | + <browser:page |
242 | + name="++ref-pending-merges" |
243 | + template="../templates/gitref-pending-merges.pt"/> |
244 | </browser:pages> |
245 | <browser:page |
246 | for="lp.code.interfaces.gitref.IGitRef" |
247 | |
248 | === modified file 'lib/lp/code/browser/gitref.py' |
249 | --- lib/lp/code/browser/gitref.py 2015-04-22 16:11:40 +0000 |
250 | +++ lib/lp/code/browser/gitref.py 2015-04-29 12:39:54 +0000 |
251 | @@ -10,12 +10,19 @@ |
252 | 'GitRefView', |
253 | ] |
254 | |
255 | +from lp.code.browser.branchmergeproposal import ( |
256 | + latest_proposals_for_each_branch, |
257 | + ) |
258 | from lp.code.interfaces.gitref import IGitRef |
259 | +from lp.code.model.gitrepository import GitRepository |
260 | +from lp.services.database.bulk import load_related |
261 | +from lp.services.propertycache import cachedproperty |
262 | from lp.services.webapp import ( |
263 | LaunchpadView, |
264 | Navigation, |
265 | stepthrough, |
266 | ) |
267 | +from lp.services.webapp.authorization import check_permission |
268 | |
269 | |
270 | class GitRefNavigation(Navigation): |
271 | @@ -49,3 +56,57 @@ |
272 | "author_date": self.context.author_date, |
273 | "commit_message": self.context.commit_message, |
274 | } |
275 | + |
276 | + @property |
277 | + def show_merge_links(self): |
278 | + """Return whether or not merge proposal links should be shown. |
279 | + |
280 | + Merge proposal links should not be shown if there is only one |
281 | + reference in the entire target. |
282 | + """ |
283 | + if not self.context.namespace.supports_merge_proposals: |
284 | + return False |
285 | + repositories = self.context.namespace.collection.getRepositories() |
286 | + if repositories.count() > 1: |
287 | + return True |
288 | + repository = repositories.one() |
289 | + if repository is None: |
290 | + return False |
291 | + return repository.refs.count() > 1 |
292 | + |
293 | + @cachedproperty |
294 | + def landing_targets(self): |
295 | + """Return a filtered list of landing targets.""" |
296 | + return latest_proposals_for_each_branch(self.context.landing_targets) |
297 | + |
298 | + @cachedproperty |
299 | + def landing_candidates(self): |
300 | + """Return a decorated list of landing candidates.""" |
301 | + candidates = list(self.context.landing_candidates) |
302 | + load_related( |
303 | + GitRepository, candidates, |
304 | + ["source_git_repositoryID", "prerequisite_git_repositoryID"]) |
305 | + return [proposal for proposal in candidates |
306 | + if check_permission("launchpad.View", proposal)] |
307 | + |
308 | + def _getBranchCountText(self, count): |
309 | + """Help to show user friendly text.""" |
310 | + if count == 0: |
311 | + return 'No branches' |
312 | + elif count == 1: |
313 | + return '1 branch' |
314 | + else: |
315 | + return '%s branches' % count |
316 | + |
317 | + @cachedproperty |
318 | + def landing_candidate_count_text(self): |
319 | + return self._getBranchCountText(len(self.landing_candidates)) |
320 | + |
321 | + @cachedproperty |
322 | + def dependent_landings(self): |
323 | + return [proposal for proposal in self.context.dependent_landings |
324 | + if check_permission("launchpad.View", proposal)] |
325 | + |
326 | + @cachedproperty |
327 | + def dependent_landing_count_text(self): |
328 | + return self._getBranchCountText(len(self.dependent_landings)) |
329 | |
330 | === modified file 'lib/lp/code/browser/tests/test_branchmergeproposal.py' |
331 | --- lib/lp/code/browser/tests/test_branchmergeproposal.py 2015-04-19 12:56:32 +0000 |
332 | +++ lib/lp/code/browser/tests/test_branchmergeproposal.py 2015-04-29 12:39:54 +0000 |
333 | @@ -46,6 +46,7 @@ |
334 | BranchMergeProposalStatus, |
335 | CodeReviewVote, |
336 | ) |
337 | +from lp.code.interfaces.gitrepository import GIT_FEATURE_FLAG |
338 | from lp.code.model.diff import PreviewDiff |
339 | from lp.code.tests.helpers import ( |
340 | add_revision_to_branch, |
341 | @@ -55,6 +56,7 @@ |
342 | PersonVisibility, |
343 | TeamMembershipPolicy, |
344 | ) |
345 | +from lp.services.features.testing import FeatureFixture |
346 | from lp.services.librarian.interfaces.client import LibrarianServerError |
347 | from lp.services.messages.model.message import MessageSet |
348 | from lp.services.webapp import canonical_url |
349 | @@ -107,8 +109,8 @@ |
350 | self.assertTrue(d.can_change_review) |
351 | |
352 | |
353 | -class TestBranchMergeProposalMergedView(TestCaseWithFactory): |
354 | - """Tests for `BranchMergeProposalMergedView`.""" |
355 | +class TestBranchMergeProposalMergedViewBzr(TestCaseWithFactory): |
356 | + """Tests for `BranchMergeProposalMergedView` for Bazaar.""" |
357 | |
358 | layer = DatabaseFunctionalLayer |
359 | |
360 | @@ -129,6 +131,30 @@ |
361 | view.initial_values) |
362 | |
363 | |
364 | +class TestBranchMergeProposalMergedViewGit(TestCaseWithFactory): |
365 | + """Tests for `BranchMergeProposalMergedView` for Git.""" |
366 | + |
367 | + layer = DatabaseFunctionalLayer |
368 | + |
369 | + def setUp(self): |
370 | + # Use an admin so we don't have to worry about launchpad.Edit |
371 | + # permissions on the merge proposals for adding comments, or |
372 | + # nominating reviewers. |
373 | + TestCaseWithFactory.setUp(self, user="admin@canonical.com") |
374 | + self.useFixture(FeatureFixture({GIT_FEATURE_FLAG: u"on"})) |
375 | + self.bmp = self.factory.makeBranchMergeProposalForGit() |
376 | + |
377 | + def test_initial_values(self): |
378 | + # The default merged_revision_id is the head commit SHA-1 of the |
379 | + # target ref. |
380 | + view = BranchMergeProposalMergedView(self.bmp, LaunchpadTestRequest()) |
381 | + removeSecurityProxy(self.bmp.source_git_ref).commit_sha1 = "0" * 40 |
382 | + removeSecurityProxy(self.bmp.target_git_ref).commit_sha1 = "1" * 40 |
383 | + self.assertEqual( |
384 | + {'merged_revision_id': self.bmp.target_git_ref.commit_sha1}, |
385 | + view.initial_values) |
386 | + |
387 | + |
388 | class TestBranchMergeProposalAddVoteView(TestCaseWithFactory): |
389 | """Test the AddVote view.""" |
390 | |
391 | @@ -661,14 +687,14 @@ |
392 | self.assertEqual(BrowserNotificationLevel.INFO, notification.level) |
393 | |
394 | |
395 | -class TestBranchMergeProposalResubmitView(TestCaseWithFactory): |
396 | +class TestBranchMergeProposalResubmitViewMixin: |
397 | """Test BranchMergeProposalResubmitView.""" |
398 | |
399 | layer = DatabaseFunctionalLayer |
400 | |
401 | def createView(self): |
402 | """Create the required view.""" |
403 | - context = self.factory.makeBranchMergeProposal() |
404 | + context = self._makeBranchMergeProposal() |
405 | self.useContext(person_logged_in(context.registrant)) |
406 | view = BranchMergeProposalResubmitView( |
407 | context, LaunchpadTestRequest()) |
408 | @@ -679,36 +705,34 @@ |
409 | """resubmit_action resubmits the proposal.""" |
410 | view = self.createView() |
411 | context = view.context |
412 | - new_proposal = view.resubmit_action.success( |
413 | - {'source_branch': context.source_branch, |
414 | - 'target_branch': context.target_branch, |
415 | - 'prerequisite_branch': context.prerequisite_branch, |
416 | - 'description': None, |
417 | - 'break_link': False, |
418 | - }) |
419 | + new_proposal = view.resubmit_action.success(self._getFormValues( |
420 | + context.merge_source, context.merge_target, |
421 | + context.merge_prerequisite, { |
422 | + 'description': None, |
423 | + 'break_link': False, |
424 | + })) |
425 | self.assertEqual(new_proposal.supersedes, context) |
426 | - self.assertEqual(new_proposal.source_branch, context.source_branch) |
427 | - self.assertEqual(new_proposal.target_branch, context.target_branch) |
428 | + self.assertEqual(new_proposal.merge_source, context.merge_source) |
429 | + self.assertEqual(new_proposal.merge_target, context.merge_target) |
430 | self.assertEqual( |
431 | - new_proposal.prerequisite_branch, context.prerequisite_branch) |
432 | + new_proposal.merge_prerequisite, context.merge_prerequisite) |
433 | |
434 | def test_resubmit_action_change_branches(self): |
435 | """Changing the branches changes the branches in the new proposal.""" |
436 | view = self.createView() |
437 | - target = view.context.source_branch.target |
438 | - new_source = self.factory.makeBranchTargetBranch(target) |
439 | - new_target = self.factory.makeBranchTargetBranch(target) |
440 | - new_prerequisite = self.factory.makeBranchTargetBranch(target) |
441 | - new_proposal = view.resubmit_action.success( |
442 | - {'source_branch': new_source, 'target_branch': new_target, |
443 | - 'prerequisite_branch': new_prerequisite, |
444 | - 'description': 'description', |
445 | - 'break_link': False, |
446 | - }) |
447 | + new_source = self._makeBranchWithSameTarget(view.context.merge_source) |
448 | + new_target = self._makeBranchWithSameTarget(view.context.merge_source) |
449 | + new_prerequisite = self._makeBranchWithSameTarget( |
450 | + view.context.merge_source) |
451 | + new_proposal = view.resubmit_action.success(self._getFormValues( |
452 | + new_source, new_target, new_prerequisite, { |
453 | + 'description': 'description', |
454 | + 'break_link': False, |
455 | + })) |
456 | self.assertEqual(new_proposal.supersedes, view.context) |
457 | - self.assertEqual(new_proposal.source_branch, new_source) |
458 | - self.assertEqual(new_proposal.target_branch, new_target) |
459 | - self.assertEqual(new_proposal.prerequisite_branch, new_prerequisite) |
460 | + self.assertEqual(new_proposal.merge_source, new_source) |
461 | + self.assertEqual(new_proposal.merge_target, new_target) |
462 | + self.assertEqual(new_proposal.merge_prerequisite, new_prerequisite) |
463 | |
464 | def test_resubmit_action_break_link(self): |
465 | """Enabling break_link prevents linking the old and new proposals.""" |
466 | @@ -716,24 +740,22 @@ |
467 | new_proposal = self.resubmitDefault(view, break_link=True) |
468 | self.assertIs(None, new_proposal.supersedes) |
469 | |
470 | - @staticmethod |
471 | - def resubmitDefault(view, break_link=False, prerequisite_branch=None): |
472 | + @classmethod |
473 | + def resubmitDefault(cls, view, break_link=False, merge_prerequisite=None): |
474 | context = view.context |
475 | - if prerequisite_branch is None: |
476 | - prerequisite_branch = context.prerequisite_branch |
477 | - return view.resubmit_action.success( |
478 | - {'source_branch': context.source_branch, |
479 | - 'target_branch': context.target_branch, |
480 | - 'prerequisite_branch': prerequisite_branch, |
481 | - 'description': None, |
482 | - 'break_link': break_link, |
483 | - }) |
484 | + if merge_prerequisite is None: |
485 | + merge_prerequisite = context.merge_prerequisite |
486 | + return view.resubmit_action.success(cls._getFormValues( |
487 | + context.merge_source, context.merge_target, merge_prerequisite, { |
488 | + 'description': None, |
489 | + 'break_link': break_link, |
490 | + })) |
491 | |
492 | def test_resubmit_existing(self): |
493 | """Resubmitting a proposal when another is active is a user error.""" |
494 | view = self.createView() |
495 | first_bmp = view.context |
496 | - with person_logged_in(first_bmp.target_branch.owner): |
497 | + with person_logged_in(first_bmp.merge_target.owner): |
498 | first_bmp.resubmit(first_bmp.registrant) |
499 | self.resubmitDefault(view) |
500 | (notification,) = view.request.response.notifications |
501 | @@ -742,19 +764,86 @@ |
502 | ' <a href=.*>a similar merge proposal</a> is already active.')) |
503 | self.assertEqual(BrowserNotificationLevel.ERROR, notification.level) |
504 | |
505 | + |
506 | +class TestBranchMergeProposalResubmitViewBzr( |
507 | + TestBranchMergeProposalResubmitViewMixin, TestCaseWithFactory): |
508 | + """Test BranchMergeProposalResubmitView for Bazaar.""" |
509 | + |
510 | + def _makeBranchMergeProposal(self): |
511 | + return self.factory.makeBranchMergeProposal() |
512 | + |
513 | + @staticmethod |
514 | + def _getFormValues(source_branch, target_branch, prerequisite_branch, |
515 | + extras): |
516 | + values = { |
517 | + 'source_branch': source_branch, |
518 | + 'target_branch': target_branch, |
519 | + 'prerequisite_branch': prerequisite_branch, |
520 | + } |
521 | + values.update(extras) |
522 | + return values |
523 | + |
524 | + def _makeBranchWithSameTarget(self, branch): |
525 | + return self.factory.makeBranchTargetBranch(branch.target) |
526 | + |
527 | def test_resubmit_same_target_prerequisite(self): |
528 | """User error if same branch is target and prerequisite.""" |
529 | view = self.createView() |
530 | first_bmp = view.context |
531 | - self.resubmitDefault( |
532 | - view, prerequisite_branch=first_bmp.target_branch) |
533 | + self.resubmitDefault(view, merge_prerequisite=first_bmp.merge_target) |
534 | self.assertEqual( |
535 | view.errors, |
536 | ['Target and prerequisite branches must be different.']) |
537 | |
538 | |
539 | -class TestResubmitBrowser(BrowserTestCase): |
540 | - """Browser tests for resubmitting branch merge proposals.""" |
541 | +class TestBranchMergeProposalResubmitViewGit( |
542 | + TestBranchMergeProposalResubmitViewMixin, TestCaseWithFactory): |
543 | + """Test BranchMergeProposalResubmitView for Git.""" |
544 | + |
545 | + def setUp(self): |
546 | + super(TestBranchMergeProposalResubmitViewGit, self).setUp() |
547 | + self.useFixture(FeatureFixture({GIT_FEATURE_FLAG: u"on"})) |
548 | + |
549 | + def _makeBranchMergeProposal(self): |
550 | + return self.factory.makeBranchMergeProposalForGit() |
551 | + |
552 | + @staticmethod |
553 | + def _getFormValues(source_branch, target_branch, prerequisite_branch, |
554 | + extras): |
555 | + values = { |
556 | + 'source_git_repository': source_branch.repository, |
557 | + 'source_git_path': source_branch.path, |
558 | + 'target_git_repository': target_branch.repository, |
559 | + 'target_git_path': target_branch.path, |
560 | + } |
561 | + if prerequisite_branch is not None: |
562 | + values.update({ |
563 | + 'prerequisite_git_repository': prerequisite_branch.repository, |
564 | + 'prerequisite_git_path': prerequisite_branch.path, |
565 | + }) |
566 | + else: |
567 | + values.update({ |
568 | + 'prerequisite_git_repository': None, |
569 | + 'prerequisite_git_path': '', |
570 | + }) |
571 | + values.update(extras) |
572 | + return values |
573 | + |
574 | + def _makeBranchWithSameTarget(self, branch): |
575 | + return self.factory.makeGitRefs(target=branch.target)[0] |
576 | + |
577 | + def test_resubmit_same_target_prerequisite(self): |
578 | + """User error if same branch is target and prerequisite.""" |
579 | + view = self.createView() |
580 | + first_bmp = view.context |
581 | + self.resubmitDefault(view, merge_prerequisite=first_bmp.merge_target) |
582 | + self.assertEqual( |
583 | + view.errors, |
584 | + ['Target and prerequisite references must be different.']) |
585 | + |
586 | + |
587 | +class TestResubmitBrowserBzr(BrowserTestCase): |
588 | + """Browser tests for resubmitting branch merge proposals for Bazaar.""" |
589 | |
590 | layer = DatabaseFunctionalLayer |
591 | |
592 | @@ -781,6 +870,41 @@ |
593 | self.assertEqual('flibble', bmp.superseded_by.description) |
594 | |
595 | |
596 | +class TestResubmitBrowserGit(BrowserTestCase): |
597 | + """Browser tests for resubmitting branch merge proposals for Git.""" |
598 | + |
599 | + layer = DatabaseFunctionalLayer |
600 | + |
601 | + def setUp(self): |
602 | + super(TestResubmitBrowserGit, self).setUp() |
603 | + self.useFixture(FeatureFixture({GIT_FEATURE_FLAG: u"on"})) |
604 | + |
605 | + def test_resubmit_text(self): |
606 | + """The text of the resubmit page is as expected.""" |
607 | + bmp = self.factory.makeBranchMergeProposalForGit(registrant=self.user) |
608 | + text = self.getMainText(bmp, '+resubmit') |
609 | + expected = ( |
610 | + 'Resubmit proposal to merge.*' |
611 | + 'Source Git Repository:.*' |
612 | + 'Source Git branch path:.*' |
613 | + 'Target Git Repository:.*' |
614 | + 'Target Git branch path:.*' |
615 | + 'Prerequisite Git Repository:.*' |
616 | + 'Prerequisite Git branch path:.*' |
617 | + 'Description.*' |
618 | + 'Start afresh.*') |
619 | + self.assertTextMatchesExpressionIgnoreWhitespace(expected, text) |
620 | + |
621 | + def test_resubmit_controls(self): |
622 | + """Proposals can be resubmitted using the browser.""" |
623 | + bmp = self.factory.makeBranchMergeProposalForGit(registrant=self.user) |
624 | + browser = self.getViewBrowser(bmp, '+resubmit') |
625 | + browser.getControl('Description').value = 'flibble' |
626 | + browser.getControl('Resubmit').click() |
627 | + with person_logged_in(self.user): |
628 | + self.assertEqual('flibble', bmp.superseded_by.description) |
629 | + |
630 | + |
631 | class TestBranchMergeProposalView(TestCaseWithFactory): |
632 | |
633 | layer = LaunchpadFunctionalLayer |
634 | @@ -1248,17 +1372,17 @@ |
635 | self.assertEqual(mp_url, browser.url) |
636 | |
637 | |
638 | -class TestLatestProposalsForEachBranch(TestCaseWithFactory): |
639 | +class TestLatestProposalsForEachBranchMixin: |
640 | """Confirm that the latest branch is returned.""" |
641 | |
642 | layer = DatabaseFunctionalLayer |
643 | |
644 | def test_newest_first(self): |
645 | # If each proposal targets a different branch, each will be returned. |
646 | - bmp1 = self.factory.makeBranchMergeProposal( |
647 | + bmp1 = self._makeBranchMergeProposal( |
648 | date_created=( |
649 | datetime(year=2008, month=9, day=10, tzinfo=pytz.UTC))) |
650 | - bmp2 = self.factory.makeBranchMergeProposal( |
651 | + bmp2 = self._makeBranchMergeProposal( |
652 | date_created=( |
653 | datetime(year=2008, month=10, day=10, tzinfo=pytz.UTC))) |
654 | self.assertEqual( |
655 | @@ -1266,27 +1390,57 @@ |
656 | |
657 | def test_visible_filtered_out(self): |
658 | # If the proposal is not visible to the user, they are not returned. |
659 | - bmp1 = self.factory.makeBranchMergeProposal( |
660 | + bmp1 = self._makeBranchMergeProposal( |
661 | date_created=( |
662 | datetime(year=2008, month=9, day=10, tzinfo=pytz.UTC))) |
663 | - bmp2 = self.factory.makeBranchMergeProposal( |
664 | + bmp2 = self._makeBranchMergeProposal( |
665 | date_created=( |
666 | datetime(year=2008, month=10, day=10, tzinfo=pytz.UTC))) |
667 | - removeSecurityProxy(bmp2.source_branch).transitionToInformationType( |
668 | - InformationType.USERDATA, bmp2.source_branch.owner, |
669 | - verify_policy=False) |
670 | + self._setBranchInvisible(bmp2.merge_source) |
671 | self.assertEqual( |
672 | [bmp1], latest_proposals_for_each_branch([bmp1, bmp2])) |
673 | |
674 | def test_same_target(self): |
675 | # If the proposals target the same branch, then the most recent is |
676 | # returned. |
677 | - bmp1 = self.factory.makeBranchMergeProposal( |
678 | + bmp1 = self._makeBranchMergeProposal( |
679 | date_created=( |
680 | datetime(year=2008, month=9, day=10, tzinfo=pytz.UTC))) |
681 | - bmp2 = self.factory.makeBranchMergeProposal( |
682 | - target_branch=bmp1.target_branch, |
683 | + bmp2 = self._makeBranchMergeProposal( |
684 | + merge_target=bmp1.merge_target, |
685 | date_created=( |
686 | datetime(year=2008, month=10, day=10, tzinfo=pytz.UTC))) |
687 | self.assertEqual( |
688 | [bmp2], latest_proposals_for_each_branch([bmp1, bmp2])) |
689 | + |
690 | + |
691 | +class TestLatestProposalsForEachBranchBzr( |
692 | + TestLatestProposalsForEachBranchMixin, TestCaseWithFactory): |
693 | + """Confirm that the latest branch is returned for Bazaar.""" |
694 | + |
695 | + def _makeBranchMergeProposal(self, merge_target=None, **kwargs): |
696 | + return self.factory.makeBranchMergeProposal( |
697 | + target_branch=merge_target, **kwargs) |
698 | + |
699 | + @staticmethod |
700 | + def _setBranchInvisible(branch): |
701 | + removeSecurityProxy(branch).transitionToInformationType( |
702 | + InformationType.USERDATA, branch.owner, verify_policy=False) |
703 | + |
704 | + |
705 | +class TestLatestProposalsForEachBranchGit( |
706 | + TestLatestProposalsForEachBranchMixin, TestCaseWithFactory): |
707 | + """Confirm that the latest branch is returned for Bazaar.""" |
708 | + |
709 | + def setUp(self): |
710 | + super(TestLatestProposalsForEachBranchGit, self).setUp() |
711 | + self.useFixture(FeatureFixture({GIT_FEATURE_FLAG: u"on"})) |
712 | + |
713 | + def _makeBranchMergeProposal(self, merge_target=None, **kwargs): |
714 | + return self.factory.makeBranchMergeProposalForGit( |
715 | + target_ref=merge_target, **kwargs) |
716 | + |
717 | + @staticmethod |
718 | + def _setBranchInvisible(branch): |
719 | + removeSecurityProxy(branch.repository).transitionToInformationType( |
720 | + InformationType.USERDATA, branch.owner, verify_policy=False) |
721 | |
722 | === modified file 'lib/lp/code/errors.py' |
723 | --- lib/lp/code/errors.py 2015-04-19 12:56:32 +0000 |
724 | +++ lib/lp/code/errors.py 2015-04-29 12:39:54 +0000 |
725 | @@ -231,11 +231,18 @@ |
726 | """Raised if there is already a matching BranchMergeProposal.""" |
727 | |
728 | def __init__(self, existing_proposal): |
729 | + # Circular import. |
730 | + from lp.code.interfaces.branch import IBranch |
731 | + # display_name is the newer style, but IBranch uses the older style. |
732 | + if IBranch.providedBy(existing_proposal.merge_source): |
733 | + display_name = "displayname" |
734 | + else: |
735 | + display_name = "display_name" |
736 | super(BranchMergeProposalExists, self).__init__( |
737 | 'There is already a branch merge proposal registered for ' |
738 | 'branch %s to land on %s that is still active.' % |
739 | - (existing_proposal.source_branch.displayname, |
740 | - existing_proposal.target_branch.displayname)) |
741 | + (getattr(existing_proposal.merge_source, display_name), |
742 | + getattr(existing_proposal.merge_target, display_name))) |
743 | self.existing_proposal = existing_proposal |
744 | |
745 | |
746 | |
747 | === modified file 'lib/lp/code/interfaces/gitrepository.py' |
748 | --- lib/lp/code/interfaces/gitrepository.py 2015-04-22 16:11:40 +0000 |
749 | +++ lib/lp/code/interfaces/gitrepository.py 2015-04-29 12:39:54 +0000 |
750 | @@ -49,6 +49,7 @@ |
751 | Choice, |
752 | Datetime, |
753 | Int, |
754 | + List, |
755 | Text, |
756 | TextLine, |
757 | ) |
758 | @@ -607,6 +608,39 @@ |
759 | :return: A collection of `IGitRepository` objects. |
760 | """ |
761 | |
762 | + @call_with(user=REQUEST_USER) |
763 | + @operation_parameters( |
764 | + person=Reference( |
765 | + title=_("The person whose repository visibility is being " |
766 | + "checked."), |
767 | + schema=IPerson), |
768 | + repository_names=List(value_type=Text(), |
769 | + title=_('List of repository unique names'), required=True), |
770 | + ) |
771 | + @export_read_operation() |
772 | + @operation_for_version("devel") |
773 | + def getRepositoryVisibilityInfo(user, person, repository_names): |
774 | + """Return the named repositories visible to both user and person. |
775 | + |
776 | + Anonymous requesters don't get any information. |
777 | + |
778 | + :param user: The user requesting the information. If the user is |
779 | + None then we return an empty dict. |
780 | + :param person: The person whose repository visibility we wish to |
781 | + check. |
782 | + :param repository_names: The unique names of the repositories to |
783 | + check. |
784 | + |
785 | + Return a dict with the following values: |
786 | + person_name: the displayname of the person. |
787 | + visible_repositories: a list of the unique names of the repositories |
788 | + which the requester and specified person can both see. |
789 | + |
790 | + This API call is provided for use by the client Javascript. It is |
791 | + not designed to efficiently scale to handle requests for large |
792 | + numbers of repositories. |
793 | + """ |
794 | + |
795 | @operation_parameters( |
796 | target=Reference( |
797 | title=_("Target"), required=True, schema=IHasGitRepositories)) |
798 | |
799 | === modified file 'lib/lp/code/model/gitref.py' |
800 | --- lib/lp/code/model/gitref.py 2015-04-24 12:58:46 +0000 |
801 | +++ lib/lp/code/model/gitref.py 2015-04-29 12:39:54 +0000 |
802 | @@ -376,3 +376,6 @@ |
803 | |
804 | def __ne__(self, other): |
805 | return not self == other |
806 | + |
807 | + def __hash__(self): |
808 | + return hash(self.repository) ^ hash(self.path) ^ hash(self.commit_sha1) |
809 | |
810 | === modified file 'lib/lp/code/model/gitrepository.py' |
811 | --- lib/lp/code/model/gitrepository.py 2015-04-22 16:42:57 +0000 |
812 | +++ lib/lp/code/model/gitrepository.py 2015-04-29 12:39:54 +0000 |
813 | @@ -38,6 +38,7 @@ |
814 | from storm.store import Store |
815 | from zope.component import getUtility |
816 | from zope.interface import implements |
817 | +from zope.security.interfaces import Unauthorized |
818 | from zope.security.proxy import removeSecurityProxy |
819 | |
820 | from lp.app.enums import ( |
821 | @@ -774,6 +775,27 @@ |
822 | collection = IGitCollection(target).visibleByUser(user) |
823 | return collection.getRepositories(eager_load=True) |
824 | |
825 | + def getRepositoryVisibilityInfo(self, user, person, repository_names): |
826 | + """See `IGitRepositorySet`.""" |
827 | + if user is None: |
828 | + return dict() |
829 | + lookup = getUtility(IGitLookup) |
830 | + visible_repositories = [] |
831 | + for name in repository_names: |
832 | + repository = lookup.getByUniqueName(name) |
833 | + try: |
834 | + if (repository is not None |
835 | + and repository.visibleByUser(user) |
836 | + and repository.visibleByUser(person)): |
837 | + visible_repositories.append(repository.unique_name) |
838 | + except Unauthorized: |
839 | + # We don't include repositories user cannot see. |
840 | + pass |
841 | + return { |
842 | + 'person_name': person.displayname, |
843 | + 'visible_repositories': visible_repositories, |
844 | + } |
845 | + |
846 | def getDefaultRepository(self, target): |
847 | """See `IGitRepositorySet`.""" |
848 | clauses = [GitRepository.target_default == True] |
849 | |
850 | === modified file 'lib/lp/code/model/tests/test_gitrepository.py' |
851 | --- lib/lp/code/model/tests/test_gitrepository.py 2015-04-21 23:05:48 +0000 |
852 | +++ lib/lp/code/model/tests/test_gitrepository.py 2015-04-29 12:39:54 +0000 |
853 | @@ -1216,6 +1216,80 @@ |
854 | public_repositories + [private_repository], |
855 | self.repository_set.getRepositories(other_person, project)) |
856 | |
857 | + def test_getRepositoryVisibilityInfo_empty_repository_names(self): |
858 | + # If repository_names is empty, getRepositoryVisibilityInfo returns |
859 | + # an empty visible_repositories list. |
860 | + person = self.factory.makePerson(name="fred") |
861 | + info = self.repository_set.getRepositoryVisibilityInfo( |
862 | + person, person, repository_names=[]) |
863 | + self.assertEqual("Fred", info["person_name"]) |
864 | + self.assertEqual([], info["visible_repositories"]) |
865 | + |
866 | + def test_getRepositoryVisibilityInfo(self): |
867 | + person = self.factory.makePerson(name="fred") |
868 | + owner = self.factory.makePerson() |
869 | + visible_repository = self.factory.makeGitRepository() |
870 | + invisible_repository = self.factory.makeGitRepository( |
871 | + owner=owner, information_type=InformationType.USERDATA) |
872 | + invisible_name = removeSecurityProxy(invisible_repository).unique_name |
873 | + repositories = [visible_repository.unique_name, invisible_name] |
874 | + |
875 | + with person_logged_in(owner): |
876 | + info = self.repository_set.getRepositoryVisibilityInfo( |
877 | + owner, person, repository_names=repositories) |
878 | + self.assertEqual("Fred", info["person_name"]) |
879 | + self.assertEqual( |
880 | + [visible_repository.unique_name], info["visible_repositories"]) |
881 | + |
882 | + def test_getRepositoryVisibilityInfo_unauthorised_user(self): |
883 | + # If the user making the API request cannot see one of the |
884 | + # repositories, that repository is not included in the results. |
885 | + person = self.factory.makePerson(name="fred") |
886 | + owner = self.factory.makePerson() |
887 | + visible_repository = self.factory.makeGitRepository() |
888 | + invisible_repository = self.factory.makeGitRepository( |
889 | + owner=owner, information_type=InformationType.USERDATA) |
890 | + invisible_name = removeSecurityProxy(invisible_repository).unique_name |
891 | + repositories = [visible_repository.unique_name, invisible_name] |
892 | + |
893 | + someone = self.factory.makePerson() |
894 | + with person_logged_in(someone): |
895 | + info = self.repository_set.getRepositoryVisibilityInfo( |
896 | + someone, person, repository_names=repositories) |
897 | + self.assertEqual("Fred", info["person_name"]) |
898 | + self.assertEqual( |
899 | + [visible_repository.unique_name], info["visible_repositories"]) |
900 | + |
901 | + def test_getRepositoryVisibilityInfo_anonymous(self): |
902 | + # Anonymous users are not allowed to see any repository visibility |
903 | + # information, even if the repository they are querying about is |
904 | + # public. |
905 | + person = self.factory.makePerson(name="fred") |
906 | + owner = self.factory.makePerson() |
907 | + visible_repository = self.factory.makeGitRepository(owner=owner) |
908 | + repositories = [visible_repository.unique_name] |
909 | + |
910 | + with person_logged_in(owner): |
911 | + info = self.repository_set.getRepositoryVisibilityInfo( |
912 | + None, person, repository_names=repositories) |
913 | + self.assertEqual({}, info) |
914 | + |
915 | + def test_getRepositoryVisibilityInfo_invalid_repository_name(self): |
916 | + # If an invalid repository name is specified, it is not included. |
917 | + person = self.factory.makePerson(name="fred") |
918 | + owner = self.factory.makePerson() |
919 | + visible_repository = self.factory.makeGitRepository(owner=owner) |
920 | + repositories = [ |
921 | + visible_repository.unique_name, |
922 | + "invalid_repository_name"] |
923 | + |
924 | + with person_logged_in(owner): |
925 | + info = self.repository_set.getRepositoryVisibilityInfo( |
926 | + owner, person, repository_names=repositories) |
927 | + self.assertEqual("Fred", info["person_name"]) |
928 | + self.assertEqual( |
929 | + [visible_repository.unique_name], info["visible_repositories"]) |
930 | + |
931 | def test_setDefaultRepository_refuses_person(self): |
932 | # setDefaultRepository refuses if the target is a person. |
933 | person = self.factory.makePerson() |
934 | |
935 | === modified file 'lib/lp/code/templates/branchmergeproposal-resubmit.pt' |
936 | --- lib/lp/code/templates/branchmergeproposal-resubmit.pt 2010-11-04 16:56:35 +0000 |
937 | +++ lib/lp/code/templates/branchmergeproposal-resubmit.pt 2015-04-29 12:39:54 +0000 |
938 | @@ -24,7 +24,7 @@ |
939 | </div> |
940 | </div> |
941 | |
942 | - <div id="source-revisions"> |
943 | + <div id="source-revisions" tal:condition="context/source_branch"> |
944 | <tal:history-available condition="context/source_branch/revision_count" |
945 | define="branch context/source_branch; |
946 | revisions view/unlanded_revisions"> |
947 | |
948 | === modified file 'lib/lp/code/templates/gitref-index.pt' |
949 | --- lib/lp/code/templates/gitref-index.pt 2015-03-19 17:04:22 +0000 |
950 | +++ lib/lp/code/templates/gitref-index.pt 2015-04-29 12:39:54 +0000 |
951 | @@ -17,6 +17,13 @@ |
952 | <div metal:fill-slot="main"> |
953 | |
954 | <div class="yui-g"> |
955 | + <div id="ref-relations" class="portlet"> |
956 | + <tal:ref-pending-merges |
957 | + replace="structure context/@@++ref-pending-merges" /> |
958 | + </div> |
959 | + </div> |
960 | + |
961 | + <div class="yui-g"> |
962 | <div id="ref-info" class="portlet"> |
963 | <h2>Branch information</h2> |
964 | <div class="two-column-list"> |
965 | |
966 | === added file 'lib/lp/code/templates/gitref-pending-merges.pt' |
967 | --- lib/lp/code/templates/gitref-pending-merges.pt 1970-01-01 00:00:00 +0000 |
968 | +++ lib/lp/code/templates/gitref-pending-merges.pt 2015-04-29 12:39:54 +0000 |
969 | @@ -0,0 +1,43 @@ |
970 | +<div |
971 | + xmlns:tal="http://xml.zope.org/namespaces/tal" |
972 | + xmlns:metal="http://xml.zope.org/namespaces/metal" |
973 | + xmlns:i18n="http://xml.zope.org/namespaces/i18n" |
974 | + tal:define=" |
975 | + context_menu view/context/menu:context; |
976 | + features request/features" |
977 | + tal:condition="view/show_merge_links"> |
978 | + |
979 | + <h3>Branch merges</h3> |
980 | + <div id="merge-links" |
981 | + class="actions"> |
982 | + <div id="merge-summary"> |
983 | + |
984 | + <div id="landing-candidates" |
985 | + tal:condition="view/landing_candidates"> |
986 | + <img src="/@@/merge-proposal-icon" /> |
987 | + <a href="+activereviews" tal:content="structure view/landing_candidate_count_text"> |
988 | + 1 branch |
989 | + </a> |
990 | + proposed for merging into this one. |
991 | + |
992 | + </div> |
993 | + |
994 | + <div id="dependent-landings" tal:condition="view/dependent_landings"> |
995 | + <img src="/@@/merge-proposal-icon" /> |
996 | + <a href="+merges" tal:content="structure view/dependent_landing_count_text"> |
997 | + 1 branch |
998 | + </a> |
999 | + dependent on this one. |
1000 | + </div> |
1001 | + |
1002 | + <div id="landing-targets" tal:condition="view/landing_targets"> |
1003 | + <tal:landing-candidates repeat="mergeproposal view/landing_targets"> |
1004 | + <tal:merge-fragment |
1005 | + tal:replace="structure mergeproposal/@@+summary-fragment"/> |
1006 | + </tal:landing-candidates> |
1007 | + </div> |
1008 | + |
1009 | + </div> |
1010 | + </div> |
1011 | + |
1012 | +</div> |
1013 | |
1014 | === modified file 'lib/lp/testing/factory.py' |
1015 | --- lib/lp/testing/factory.py 2015-04-22 16:11:40 +0000 |
1016 | +++ lib/lp/testing/factory.py 2015-04-29 12:39:54 +0000 |
1017 | @@ -1747,7 +1747,8 @@ |
1018 | u"type": GitObjectType.COMMIT, |
1019 | } |
1020 | for path in paths} |
1021 | - return repository.createOrUpdateRefs(refs_info, get_objects=True) |
1022 | + return removeSecurityProxy(repository).createOrUpdateRefs( |
1023 | + refs_info, get_objects=True) |
1024 | |
1025 | def makeBug(self, target=None, owner=None, bug_watch_url=None, |
1026 | information_type=None, date_closed=None, title=None, |
One bit of useless code, otherwise looks good.