Merge lp:~cjwatson/launchpad/git-mp-basic-browser into lp:launchpad
- git-mp-basic-browser
- Merge into devel
Proposed by
Colin Watson
on 2015-04-28
| 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 | code | 2015-04-28 | Approve on 2015-04-29 |
|
Review via email:
|
|||
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.