Merge lp:~cjwatson/launchpad/relax-personal-git-mp-restrictions into lp:launchpad
- relax-personal-git-mp-restrictions
- Merge into devel
Status: | Merged |
---|---|
Merged at revision: | 18726 |
Proposed branch: | lp:~cjwatson/launchpad/relax-personal-git-mp-restrictions |
Merge into: | lp:launchpad |
Diff against target: |
585 lines (+320/-60) 7 files modified
lib/lp/app/widgets/suggestion.py (+39/-24) lib/lp/app/widgets/tests/test_suggestion.py (+58/-20) lib/lp/code/interfaces/gitnamespace.py (+7/-3) lib/lp/code/model/gitnamespace.py (+18/-8) lib/lp/code/model/gitrepository.py (+2/-2) lib/lp/code/model/tests/test_gitnamespace.py (+183/-1) lib/lp/code/model/tests/test_gitref.py (+13/-2) |
To merge this branch: | bzr merge lp:~cjwatson/launchpad/relax-personal-git-mp-restrictions |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+349253@code.launchpad.net |
Commit message
Allow proposing merges between different branches of the same personal Git repository.
Description of the change
Colin Watson (cjwatson) wrote : | # |
> Why is the suggestion widget now sometimes called on a GitRef rather than a
> GitRepository?
It was in fact always that way: it's essentially just because this widget is used on GitRef:
I think it's reasonably worth handling both even though we only use the GitRef case today, since we often want to expose similar operations on GitRef and GitRepository: for example, once we have a better ref picker I can well imagine wanting to add GitRepository:
Preview Diff
1 | === modified file 'lib/lp/app/widgets/suggestion.py' | |||
2 | --- lib/lp/app/widgets/suggestion.py 2016-01-12 12:28:09 +0000 | |||
3 | +++ lib/lp/app/widgets/suggestion.py 2018-07-11 08:48:06 +0000 | |||
4 | @@ -1,4 +1,4 @@ | |||
6 | 1 | # Copyright 2009-2016 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2018 Canonical Ltd. This software is licensed under the |
7 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
8 | 3 | 3 | ||
9 | 4 | """Widgets related to IBranch.""" | 4 | """Widgets related to IBranch.""" |
10 | @@ -34,7 +34,9 @@ | |||
11 | 34 | 34 | ||
12 | 35 | from lp.app.widgets.itemswidgets import LaunchpadRadioWidget | 35 | from lp.app.widgets.itemswidgets import LaunchpadRadioWidget |
13 | 36 | from lp.code.interfaces.gitcollection import IGitCollection | 36 | from lp.code.interfaces.gitcollection import IGitCollection |
14 | 37 | from lp.code.interfaces.gitref import IGitRef | ||
15 | 37 | from lp.code.interfaces.gitrepository import IGitRepositorySet | 38 | from lp.code.interfaces.gitrepository import IGitRepositorySet |
16 | 39 | from lp.registry.interfaces.person import IPerson | ||
17 | 38 | from lp.services.webapp import canonical_url | 40 | from lp.services.webapp import canonical_url |
18 | 39 | from lp.services.webapp.escaping import ( | 41 | from lp.services.webapp.escaping import ( |
19 | 40 | html_escape, | 42 | html_escape, |
20 | @@ -295,31 +297,42 @@ | |||
21 | 295 | """ | 297 | """ |
22 | 296 | 298 | ||
23 | 297 | @staticmethod | 299 | @staticmethod |
25 | 298 | def _generateSuggestionVocab(repository, full_vocabulary): | 300 | def _generateSuggestionVocab(context, full_vocabulary): |
26 | 299 | """Generate the vocabulary for the radio buttons. | 301 | """Generate the vocabulary for the radio buttons. |
27 | 300 | 302 | ||
28 | 301 | The generated vocabulary contains the default repository for the | 303 | The generated vocabulary contains the default repository for the |
29 | 302 | target if there is one, and also any other repositories that the | 304 | target if there is one, and also any other repositories that the |
30 | 303 | user has specified recently as a target for a proposed merge. | 305 | user has specified recently as a target for a proposed merge. |
31 | 304 | """ | 306 | """ |
50 | 305 | default_target = getUtility(IGitRepositorySet).getDefaultRepository( | 307 | if IGitRef.providedBy(context): |
51 | 306 | repository.target) | 308 | repository = context.repository |
52 | 307 | logged_in_user = getUtility(ILaunchBag).user | 309 | else: |
53 | 308 | since = datetime.now(utc) - timedelta(days=90) | 310 | repository = context |
54 | 309 | collection = IGitCollection(repository.target).targetedBy( | 311 | |
55 | 310 | logged_in_user, since) | 312 | if IPerson.providedBy(repository.target): |
56 | 311 | collection = collection.visibleByUser(logged_in_user) | 313 | # If the source is a personal repository, then the only valid |
57 | 312 | # May actually need some eager loading, but the API isn't fine grained | 314 | # target is that same repository. |
58 | 313 | # yet. | 315 | target_repositories = [repository] |
59 | 314 | repositories = collection.getRepositories(eager_load=False).config( | 316 | else: |
60 | 315 | distinct=True) | 317 | repository_set = getUtility(IGitRepositorySet) |
61 | 316 | target_repositories = list(repositories.config(limit=5)) | 318 | default_target = repository_set.getDefaultRepository( |
62 | 317 | # If there is a default repository, make sure it is always shown, | 319 | repository.target) |
63 | 318 | # and as the first item. | 320 | logged_in_user = getUtility(ILaunchBag).user |
64 | 319 | if default_target is not None: | 321 | since = datetime.now(utc) - timedelta(days=90) |
65 | 320 | if default_target in target_repositories: | 322 | collection = IGitCollection(repository.target).targetedBy( |
66 | 321 | target_repositories.remove(default_target) | 323 | logged_in_user, since) |
67 | 322 | target_repositories.insert(0, default_target) | 324 | collection = collection.visibleByUser(logged_in_user) |
68 | 325 | # May actually need some eager loading, but the API isn't fine | ||
69 | 326 | # grained yet. | ||
70 | 327 | repositories = collection.getRepositories(eager_load=False).config( | ||
71 | 328 | distinct=True) | ||
72 | 329 | target_repositories = list(repositories.config(limit=5)) | ||
73 | 330 | # If there is a default repository, make sure it is always | ||
74 | 331 | # shown, and as the first item. | ||
75 | 332 | if default_target is not None: | ||
76 | 333 | if default_target in target_repositories: | ||
77 | 334 | target_repositories.remove(default_target) | ||
78 | 335 | target_repositories.insert(0, default_target) | ||
79 | 323 | 336 | ||
80 | 324 | terms = [] | 337 | terms = [] |
81 | 325 | for repository in target_repositories: | 338 | for repository in target_repositories: |
82 | @@ -335,10 +348,12 @@ | |||
83 | 335 | # instead to have a separate link to the repository details. | 348 | # instead to have a separate link to the repository details. |
84 | 336 | text = u'%s (<a href="%s">repository details</a>)' | 349 | text = u'%s (<a href="%s">repository details</a>)' |
85 | 337 | # If the repository is the default for the target, say so. | 350 | # If the repository is the default for the target, say so. |
90 | 338 | default_target = getUtility(IGitRepositorySet).getDefaultRepository( | 351 | if not IPerson.providedBy(repository.target): |
91 | 339 | repository.target) | 352 | repository_set = getUtility(IGitRepositorySet) |
92 | 340 | if repository == default_target: | 353 | default_target = repository_set.getDefaultRepository( |
93 | 341 | text += u"– <em>default repository</em>" | 354 | repository.target) |
94 | 355 | if repository == default_target: | ||
95 | 356 | text += u"– <em>default repository</em>" | ||
96 | 342 | label = ( | 357 | label = ( |
97 | 343 | u'<label for="%s" style="font-weight: normal">' + text + | 358 | u'<label for="%s" style="font-weight: normal">' + text + |
98 | 344 | u'</label>') | 359 | u'</label>') |
99 | 345 | 360 | ||
100 | === modified file 'lib/lp/app/widgets/tests/test_suggestion.py' | |||
101 | --- lib/lp/app/widgets/tests/test_suggestion.py 2015-07-08 16:05:11 +0000 | |||
102 | +++ lib/lp/app/widgets/tests/test_suggestion.py 2018-07-11 08:48:06 +0000 | |||
103 | @@ -1,4 +1,4 @@ | |||
105 | 1 | # Copyright 2011-2015 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2011-2018 Canonical Ltd. This software is licensed under the |
106 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
107 | 3 | 3 | ||
108 | 4 | __metaclass__ = type | 4 | __metaclass__ = type |
109 | @@ -199,11 +199,11 @@ | |||
110 | 199 | DocTestMatches(expected, self.doctest_opts)) | 199 | DocTestMatches(expected, self.doctest_opts)) |
111 | 200 | 200 | ||
112 | 201 | 201 | ||
116 | 202 | def make_target_git_repository_widget(repository): | 202 | def make_target_git_repository_widget(context): |
117 | 203 | """Given a Git repository, return a widget for selecting where to land | 203 | """Given a Git repository or reference, return a widget for selecting |
118 | 204 | it.""" | 204 | where to land it.""" |
119 | 205 | choice = Choice(__name__='test_field', vocabulary='GitRepository') | 205 | choice = Choice(__name__='test_field', vocabulary='GitRepository') |
121 | 206 | choice = choice.bind(repository) | 206 | choice = choice.bind(context) |
122 | 207 | request = LaunchpadTestRequest() | 207 | request = LaunchpadTestRequest() |
123 | 208 | return TargetGitRepositoryWidget(choice, None, request) | 208 | return TargetGitRepositoryWidget(choice, None, request) |
124 | 209 | 209 | ||
125 | @@ -216,41 +216,79 @@ | |||
126 | 216 | doctest.NORMALIZE_WHITESPACE | doctest.REPORT_NDIFF | | 216 | doctest.NORMALIZE_WHITESPACE | doctest.REPORT_NDIFF | |
127 | 217 | doctest.ELLIPSIS) | 217 | doctest.ELLIPSIS) |
128 | 218 | 218 | ||
131 | 219 | def makeRepositoryAndOldMergeProposal(self, timedelta): | 219 | def makeRefAndOldMergeProposal(self, timedelta): |
132 | 220 | """Make an old merge proposal and a repository with the same target.""" | 220 | """Make an old merge proposal and a ref with the same target.""" |
133 | 221 | bmp = self.factory.makeBranchMergeProposalForGit( | 221 | bmp = self.factory.makeBranchMergeProposalForGit( |
134 | 222 | date_created=datetime.now(utc) - timedelta) | 222 | date_created=datetime.now(utc) - timedelta) |
135 | 223 | login_person(bmp.registrant) | 223 | login_person(bmp.registrant) |
138 | 224 | target = bmp.target_git_repository | 224 | target = bmp.merge_target |
139 | 225 | return target, self.factory.makeGitRepository(target=target.target) | 225 | return target, self.factory.makeGitRefs(target=target.target)[0] |
140 | 226 | 226 | ||
141 | 227 | def test_recent_target(self): | 227 | def test_recent_target(self): |
142 | 228 | """Targets for proposals newer than 90 days are included.""" | 228 | """Targets for proposals newer than 90 days are included.""" |
145 | 229 | target, source = self.makeRepositoryAndOldMergeProposal( | 229 | target, source = self.makeRefAndOldMergeProposal(timedelta(days=89)) |
144 | 230 | timedelta(days=89)) | ||
146 | 231 | widget = make_target_git_repository_widget(source) | 230 | widget = make_target_git_repository_widget(source) |
148 | 232 | self.assertIn(target, widget.suggestion_vocab) | 231 | self.assertIn(target.repository, widget.suggestion_vocab) |
149 | 233 | 232 | ||
150 | 234 | def test_stale_target(self): | 233 | def test_stale_target(self): |
151 | 235 | """Targets for proposals older than 90 days are not considered.""" | 234 | """Targets for proposals older than 90 days are not considered.""" |
156 | 236 | target, source = self.makeRepositoryAndOldMergeProposal( | 235 | target, source = self.makeRefAndOldMergeProposal(timedelta(days=91)) |
157 | 237 | timedelta(days=91)) | 236 | widget = make_target_git_repository_widget(source) |
158 | 238 | widget = make_target_git_repository_widget(source) | 237 | self.assertNotIn(target.repository, widget.suggestion_vocab) |
159 | 239 | self.assertNotIn(target, widget.suggestion_vocab) | 238 | |
160 | 239 | def test_personal_repository(self): | ||
161 | 240 | """Proposals for personal repositories only suggest that repository.""" | ||
162 | 241 | owner = self.factory.makePerson() | ||
163 | 242 | this_source, this_target = self.factory.makeGitRefs( | ||
164 | 243 | owner=owner, target=owner, | ||
165 | 244 | paths=[u"refs/heads/source", u"refs/heads/target"]) | ||
166 | 245 | bmp = self.factory.makeBranchMergeProposalForGit( | ||
167 | 246 | source_ref=this_source, target_ref=this_target, | ||
168 | 247 | date_created=datetime.now(utc) - timedelta(days=1)) | ||
169 | 248 | other_source, other_target = self.factory.makeGitRefs( | ||
170 | 249 | owner=owner, target=owner, | ||
171 | 250 | paths=[u"refs/heads/source", u"refs/heads/target"]) | ||
172 | 251 | self.factory.makeBranchMergeProposalForGit( | ||
173 | 252 | source_ref=other_source, target_ref=other_target, | ||
174 | 253 | date_created=datetime.now(utc) - timedelta(days=1)) | ||
175 | 254 | login_person(bmp.registrant) | ||
176 | 255 | [source] = self.factory.makeGitRefs(repository=this_target.repository) | ||
177 | 256 | widget = make_target_git_repository_widget(source) | ||
178 | 257 | self.assertContentEqual( | ||
179 | 258 | [this_target.repository], | ||
180 | 259 | [term.value for term in widget.suggestion_vocab]) | ||
181 | 240 | 260 | ||
182 | 241 | def test__renderSuggestionLabel(self): | 261 | def test__renderSuggestionLabel(self): |
183 | 242 | """Git repositories have a reasonable suggestion label.""" | 262 | """Git repositories have a reasonable suggestion label.""" |
186 | 243 | target, source = self.makeRepositoryAndOldMergeProposal( | 263 | target, source = self.makeRefAndOldMergeProposal(timedelta(days=1)) |
185 | 244 | timedelta(days=1)) | ||
187 | 245 | login_person(target.target.owner) | 264 | login_person(target.target.owner) |
188 | 246 | getUtility(IGitRepositorySet).setDefaultRepository( | 265 | getUtility(IGitRepositorySet).setDefaultRepository( |
190 | 247 | target.target, target) | 266 | target.target, target.repository) |
191 | 248 | widget = make_target_git_repository_widget(source) | 267 | widget = make_target_git_repository_widget(source) |
192 | 249 | expected = ( | 268 | expected = ( |
193 | 250 | """<label for="field.test_field.2" | 269 | """<label for="field.test_field.2" |
194 | 251 | ...>... (<a href="...">repository details</a>)– | 270 | ...>... (<a href="...">repository details</a>)– |
195 | 252 | <em>default repository</em></label>""") | 271 | <em>default repository</em></label>""") |
197 | 253 | structured_string = widget._renderSuggestionLabel(target, 2) | 272 | structured_string = widget._renderSuggestionLabel(target.repository, 2) |
198 | 273 | self.assertThat( | ||
199 | 274 | structured_string.escapedtext, | ||
200 | 275 | DocTestMatches(expected, self.doctest_opts)) | ||
201 | 276 | |||
202 | 277 | def test__renderSuggestionLabel_personal(self): | ||
203 | 278 | """Personal Git repositories have a reasonable suggestion label.""" | ||
204 | 279 | owner = self.factory.makePerson() | ||
205 | 280 | source, target = self.factory.makeGitRefs( | ||
206 | 281 | owner=owner, target=owner, | ||
207 | 282 | paths=[u"refs/heads/source", u"refs/heads/target"]) | ||
208 | 283 | bmp = self.factory.makeBranchMergeProposalForGit( | ||
209 | 284 | source_ref=source, target_ref=target, | ||
210 | 285 | date_created=datetime.now(utc) - timedelta(days=1)) | ||
211 | 286 | login_person(bmp.registrant) | ||
212 | 287 | widget = make_target_git_repository_widget(source) | ||
213 | 288 | expected = ( | ||
214 | 289 | """<label for="field.test_field.2" | ||
215 | 290 | ...>... (<a href="...">repository details</a>)</label>""") | ||
216 | 291 | structured_string = widget._renderSuggestionLabel(target.repository, 2) | ||
217 | 254 | self.assertThat( | 292 | self.assertThat( |
218 | 255 | structured_string.escapedtext, | 293 | structured_string.escapedtext, |
219 | 256 | DocTestMatches(expected, self.doctest_opts)) | 294 | DocTestMatches(expected, self.doctest_opts)) |
220 | 257 | 295 | ||
221 | === modified file 'lib/lp/code/interfaces/gitnamespace.py' | |||
222 | --- lib/lp/code/interfaces/gitnamespace.py 2016-10-14 17:25:51 +0000 | |||
223 | +++ lib/lp/code/interfaces/gitnamespace.py 2018-07-11 08:48:06 +0000 | |||
224 | @@ -1,4 +1,4 @@ | |||
226 | 1 | # Copyright 2015-2016 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2015-2018 Canonical Ltd. This software is licensed under the |
227 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
228 | 3 | 3 | ||
229 | 4 | """Interface for a Git repository namespace.""" | 4 | """Interface for a Git repository namespace.""" |
230 | @@ -178,8 +178,12 @@ | |||
231 | 178 | already exists in the namespace. | 178 | already exists in the namespace. |
232 | 179 | """ | 179 | """ |
233 | 180 | 180 | ||
236 | 181 | def areRepositoriesMergeable(other_namespace): | 181 | def areRepositoriesMergeable(this, other): |
237 | 182 | """Are repositories from other_namespace mergeable into this one?""" | 182 | """Is `other` mergeable into `this`? |
238 | 183 | |||
239 | 184 | :param this: An `IGitRepository` in this namespace. | ||
240 | 185 | :param other: An `IGitRepository` in either this or another namespace. | ||
241 | 186 | """ | ||
242 | 183 | 187 | ||
243 | 184 | collection = Attribute("An `IGitCollection` for this namespace.") | 188 | collection = Attribute("An `IGitCollection` for this namespace.") |
244 | 185 | 189 | ||
245 | 186 | 190 | ||
246 | === modified file 'lib/lp/code/model/gitnamespace.py' | |||
247 | --- lib/lp/code/model/gitnamespace.py 2018-05-14 08:01:56 +0000 | |||
248 | +++ lib/lp/code/model/gitnamespace.py 2018-07-11 08:48:06 +0000 | |||
249 | @@ -1,4 +1,4 @@ | |||
251 | 1 | # Copyright 2015-2016 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2015-2018 Canonical Ltd. This software is licensed under the |
252 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
253 | 3 | 3 | ||
254 | 4 | """Implementations of `IGitNamespace`.""" | 4 | """Implementations of `IGitNamespace`.""" |
255 | @@ -250,7 +250,7 @@ | |||
256 | 250 | 250 | ||
257 | 251 | has_defaults = False | 251 | has_defaults = False |
258 | 252 | allow_push_to_set_default = False | 252 | allow_push_to_set_default = False |
260 | 253 | supports_merge_proposals = False | 253 | supports_merge_proposals = True |
261 | 254 | supports_code_imports = False | 254 | supports_code_imports = False |
262 | 255 | allow_recipe_name_from_target = False | 255 | allow_recipe_name_from_target = False |
263 | 256 | 256 | ||
264 | @@ -303,15 +303,17 @@ | |||
265 | 303 | else: | 303 | else: |
266 | 304 | return InformationType.PUBLIC | 304 | return InformationType.PUBLIC |
267 | 305 | 305 | ||
269 | 306 | def areRepositoriesMergeable(self, other_namespace): | 306 | def areRepositoriesMergeable(self, this, other): |
270 | 307 | """See `IGitNamespacePolicy`.""" | 307 | """See `IGitNamespacePolicy`.""" |
272 | 308 | return False | 308 | if this.namespace != self: |
273 | 309 | raise AssertionError( | ||
274 | 310 | "Namespace of %s is not %s." % (this.unique_name, self.name)) | ||
275 | 311 | return this == other | ||
276 | 309 | 312 | ||
277 | 310 | @property | 313 | @property |
278 | 311 | def collection(self): | 314 | def collection(self): |
279 | 312 | """See `IGitNamespacePolicy`.""" | 315 | """See `IGitNamespacePolicy`.""" |
282 | 313 | return getUtility(IAllGitRepositories).ownedBy( | 316 | return getUtility(IAllGitRepositories).ownedBy(self.owner).isPersonal() |
281 | 314 | self.person).isPersonal() | ||
283 | 315 | 317 | ||
284 | 316 | def assignKarma(self, person, action_name, date_created=None): | 318 | def assignKarma(self, person, action_name, date_created=None): |
285 | 317 | """See `IGitNamespacePolicy`.""" | 319 | """See `IGitNamespacePolicy`.""" |
286 | @@ -383,12 +385,16 @@ | |||
287 | 383 | return None | 385 | return None |
288 | 384 | return default_type | 386 | return default_type |
289 | 385 | 387 | ||
291 | 386 | def areRepositoriesMergeable(self, other_namespace): | 388 | def areRepositoriesMergeable(self, this, other): |
292 | 387 | """See `IGitNamespacePolicy`.""" | 389 | """See `IGitNamespacePolicy`.""" |
293 | 388 | # Repositories are mergeable into a project repository if the | 390 | # Repositories are mergeable into a project repository if the |
294 | 389 | # project is the same. | 391 | # project is the same. |
295 | 390 | # XXX cjwatson 2015-04-18: Allow merging from a package repository | 392 | # XXX cjwatson 2015-04-18: Allow merging from a package repository |
296 | 391 | # if any (active?) series is linked to this project. | 393 | # if any (active?) series is linked to this project. |
297 | 394 | if this.namespace != self: | ||
298 | 395 | raise AssertionError( | ||
299 | 396 | "Namespace of %s is not %s." % (this.unique_name, self.name)) | ||
300 | 397 | other_namespace = other.namespace | ||
301 | 392 | if zope_isinstance(other_namespace, ProjectGitNamespace): | 398 | if zope_isinstance(other_namespace, ProjectGitNamespace): |
302 | 393 | return self.target == other_namespace.target | 399 | return self.target == other_namespace.target |
303 | 394 | else: | 400 | else: |
304 | @@ -457,12 +463,16 @@ | |||
305 | 457 | """See `IGitNamespace`.""" | 463 | """See `IGitNamespace`.""" |
306 | 458 | return InformationType.PUBLIC | 464 | return InformationType.PUBLIC |
307 | 459 | 465 | ||
309 | 460 | def areRepositoriesMergeable(self, other_namespace): | 466 | def areRepositoriesMergeable(self, this, other): |
310 | 461 | """See `IGitNamespacePolicy`.""" | 467 | """See `IGitNamespacePolicy`.""" |
311 | 462 | # Repositories are mergeable into a package repository if the | 468 | # Repositories are mergeable into a package repository if the |
312 | 463 | # package is the same. | 469 | # package is the same. |
313 | 464 | # XXX cjwatson 2015-04-18: Allow merging from a project repository | 470 | # XXX cjwatson 2015-04-18: Allow merging from a project repository |
314 | 465 | # if any (active?) series links this package to that project. | 471 | # if any (active?) series links this package to that project. |
315 | 472 | if this.namespace != self: | ||
316 | 473 | raise AssertionError( | ||
317 | 474 | "Namespace of %s is not %s." % (this.unique_name, self.name)) | ||
318 | 475 | other_namespace = other.namespace | ||
319 | 466 | if zope_isinstance(other_namespace, PackageGitNamespace): | 476 | if zope_isinstance(other_namespace, PackageGitNamespace): |
320 | 467 | return self.target == other_namespace.target | 477 | return self.target == other_namespace.target |
321 | 468 | else: | 478 | else: |
322 | 469 | 479 | ||
323 | === modified file 'lib/lp/code/model/gitrepository.py' | |||
324 | --- lib/lp/code/model/gitrepository.py 2017-11-24 17:22:34 +0000 | |||
325 | +++ lib/lp/code/model/gitrepository.py 2018-07-11 08:48:06 +0000 | |||
326 | @@ -1,4 +1,4 @@ | |||
328 | 1 | # Copyright 2015-2017 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2015-2018 Canonical Ltd. This software is licensed under the |
329 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
330 | 3 | 3 | ||
331 | 4 | __metaclass__ = type | 4 | __metaclass__ = type |
332 | @@ -946,7 +946,7 @@ | |||
333 | 946 | 946 | ||
334 | 947 | def isRepositoryMergeable(self, other): | 947 | def isRepositoryMergeable(self, other): |
335 | 948 | """See `IGitRepository`.""" | 948 | """See `IGitRepository`.""" |
337 | 949 | return self.namespace.areRepositoriesMergeable(other.namespace) | 949 | return self.namespace.areRepositoriesMergeable(self, other) |
338 | 950 | 950 | ||
339 | 951 | @property | 951 | @property |
340 | 952 | def pending_updates(self): | 952 | def pending_updates(self): |
341 | 953 | 953 | ||
342 | === modified file 'lib/lp/code/model/tests/test_gitnamespace.py' | |||
343 | --- lib/lp/code/model/tests/test_gitnamespace.py 2017-10-04 01:29:35 +0000 | |||
344 | +++ lib/lp/code/model/tests/test_gitnamespace.py 2018-07-11 08:48:06 +0000 | |||
345 | @@ -1,4 +1,4 @@ | |||
347 | 1 | # Copyright 2015-2017 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2015-2018 Canonical Ltd. This software is licensed under the |
348 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
349 | 3 | 3 | ||
350 | 4 | """Tests for `IGitNamespace` implementations.""" | 4 | """Tests for `IGitNamespace` implementations.""" |
351 | @@ -278,6 +278,70 @@ | |||
352 | 278 | namespace = PersonalGitNamespace(person) | 278 | namespace = PersonalGitNamespace(person) |
353 | 279 | self.assertEqual(person, namespace.target) | 279 | self.assertEqual(person, namespace.target) |
354 | 280 | 280 | ||
355 | 281 | def test_supports_merge_proposals(self): | ||
356 | 282 | # Personal namespaces support merge proposals. | ||
357 | 283 | self.assertTrue(self.getNamespace().supports_merge_proposals) | ||
358 | 284 | |||
359 | 285 | def test_areRepositoriesMergeable_same_repository(self): | ||
360 | 286 | # A personal repository is mergeable into itself. | ||
361 | 287 | owner = self.factory.makePerson() | ||
362 | 288 | repository = self.factory.makeGitRepository(owner=owner, target=owner) | ||
363 | 289 | self.assertTrue( | ||
364 | 290 | repository.namespace.areRepositoriesMergeable( | ||
365 | 291 | repository, repository)) | ||
366 | 292 | |||
367 | 293 | def test_areRepositoriesMergeable_same_namespace(self): | ||
368 | 294 | # A personal repository is not mergeable into another personal | ||
369 | 295 | # repository, even if they are in the same namespace. | ||
370 | 296 | owner = self.factory.makePerson() | ||
371 | 297 | this = self.factory.makeGitRepository(owner=owner, target=owner) | ||
372 | 298 | other = self.factory.makeGitRepository(owner=owner, target=owner) | ||
373 | 299 | self.assertFalse(this.namespace.areRepositoriesMergeable(this, other)) | ||
374 | 300 | |||
375 | 301 | def test_areRepositoriesMergeable_different_namespace(self): | ||
376 | 302 | # A personal repository is not mergeable into another personal | ||
377 | 303 | # repository with a different namespace. | ||
378 | 304 | this_owner = self.factory.makePerson() | ||
379 | 305 | this = self.factory.makeGitRepository( | ||
380 | 306 | owner=this_owner, target=this_owner) | ||
381 | 307 | other_owner = self.factory.makePerson() | ||
382 | 308 | other = self.factory.makeGitRepository( | ||
383 | 309 | owner=other_owner, target=other_owner) | ||
384 | 310 | self.assertFalse(this.namespace.areRepositoriesMergeable(this, other)) | ||
385 | 311 | |||
386 | 312 | def test_areRepositoriesMergeable_project(self): | ||
387 | 313 | # Project repositories are not mergeable into personal repositories. | ||
388 | 314 | owner = self.factory.makePerson() | ||
389 | 315 | this = self.factory.makeGitRepository(owner=owner, target=owner) | ||
390 | 316 | project = self.factory.makeProduct() | ||
391 | 317 | other = self.factory.makeGitRepository(owner=owner, target=project) | ||
392 | 318 | self.assertFalse(this.namespace.areRepositoriesMergeable(this, other)) | ||
393 | 319 | |||
394 | 320 | def test_areRepositoriesMergeable_package(self): | ||
395 | 321 | # Package repositories are not mergeable into personal repositories. | ||
396 | 322 | owner = self.factory.makePerson() | ||
397 | 323 | this = self.factory.makeGitRepository(owner=owner, target=owner) | ||
398 | 324 | dsp = self.factory.makeDistributionSourcePackage() | ||
399 | 325 | other = self.factory.makeGitRepository(owner=owner, target=dsp) | ||
400 | 326 | self.assertFalse(this.namespace.areRepositoriesMergeable(this, other)) | ||
401 | 327 | |||
402 | 328 | def test_collection(self): | ||
403 | 329 | # A personal namespace's collection is of personal repositories with | ||
404 | 330 | # the same owner. | ||
405 | 331 | owner = self.factory.makePerson() | ||
406 | 332 | repositories = [ | ||
407 | 333 | self.factory.makeGitRepository(owner=owner, target=owner) | ||
408 | 334 | for _ in range(3)] | ||
409 | 335 | other_owner = self.factory.makePerson() | ||
410 | 336 | self.factory.makeGitRepository(owner=other_owner, target=other_owner) | ||
411 | 337 | self.factory.makeGitRepository( | ||
412 | 338 | owner=owner, target=self.factory.makeProduct()) | ||
413 | 339 | self.factory.makeGitRepository( | ||
414 | 340 | owner=owner, target=self.factory.makeDistributionSourcePackage()) | ||
415 | 341 | self.assertContentEqual( | ||
416 | 342 | repositories, | ||
417 | 343 | repositories[0].namespace.collection.getRepositories()) | ||
418 | 344 | |||
419 | 281 | 345 | ||
420 | 282 | class TestProjectGitNamespace(TestCaseWithFactory, NamespaceMixin): | 346 | class TestProjectGitNamespace(TestCaseWithFactory, NamespaceMixin): |
421 | 283 | """Tests for `ProjectGitNamespace`.""" | 347 | """Tests for `ProjectGitNamespace`.""" |
422 | @@ -312,6 +376,65 @@ | |||
423 | 312 | namespace = ProjectGitNamespace(person, project) | 376 | namespace = ProjectGitNamespace(person, project) |
424 | 313 | self.assertEqual(project, namespace.target) | 377 | self.assertEqual(project, namespace.target) |
425 | 314 | 378 | ||
426 | 379 | def test_supports_merge_proposals(self): | ||
427 | 380 | # Project namespaces support merge proposals. | ||
428 | 381 | self.assertTrue(self.getNamespace().supports_merge_proposals) | ||
429 | 382 | |||
430 | 383 | def test_areRepositoriesMergeable_same_repository(self): | ||
431 | 384 | # A project repository is mergeable into itself. | ||
432 | 385 | project = self.factory.makeProduct() | ||
433 | 386 | repository = self.factory.makeGitRepository(target=project) | ||
434 | 387 | self.assertTrue( | ||
435 | 388 | repository.namespace.areRepositoriesMergeable( | ||
436 | 389 | repository, repository)) | ||
437 | 390 | |||
438 | 391 | def test_areRepositoriesMergeable_same_namespace(self): | ||
439 | 392 | # Repositories of the same project are mergeable. | ||
440 | 393 | project = self.factory.makeProduct() | ||
441 | 394 | this = self.factory.makeGitRepository(target=project) | ||
442 | 395 | other = self.factory.makeGitRepository(target=project) | ||
443 | 396 | self.assertTrue(this.namespace.areRepositoriesMergeable(this, other)) | ||
444 | 397 | |||
445 | 398 | def test_areRepositoriesMergeable_different_namespace(self): | ||
446 | 399 | # Repositories of a different project are not mergeable. | ||
447 | 400 | this_project = self.factory.makeProduct() | ||
448 | 401 | this = self.factory.makeGitRepository(target=this_project) | ||
449 | 402 | other_project = self.factory.makeProduct() | ||
450 | 403 | other = self.factory.makeGitRepository(target=other_project) | ||
451 | 404 | self.assertFalse(this.namespace.areRepositoriesMergeable(this, other)) | ||
452 | 405 | |||
453 | 406 | def test_areRepositoriesMergeable_personal(self): | ||
454 | 407 | # Personal repositories are not mergeable into project repositories. | ||
455 | 408 | owner = self.factory.makePerson() | ||
456 | 409 | project = self.factory.makeProduct() | ||
457 | 410 | this = self.factory.makeGitRepository(owner=owner, target=project) | ||
458 | 411 | other = self.factory.makeGitRepository(owner=owner, target=owner) | ||
459 | 412 | self.assertFalse(this.namespace.areRepositoriesMergeable(this, other)) | ||
460 | 413 | |||
461 | 414 | def test_areRepositoriesMergeable_package(self): | ||
462 | 415 | # Package repositories are not mergeable into project repositories. | ||
463 | 416 | owner = self.factory.makePerson() | ||
464 | 417 | project = self.factory.makeProduct() | ||
465 | 418 | this = self.factory.makeGitRepository(owner=owner, target=project) | ||
466 | 419 | dsp = self.factory.makeDistributionSourcePackage() | ||
467 | 420 | other = self.factory.makeGitRepository(owner=owner, target=dsp) | ||
468 | 421 | self.assertFalse(this.namespace.areRepositoriesMergeable(this, other)) | ||
469 | 422 | |||
470 | 423 | def test_collection(self): | ||
471 | 424 | # A project namespace's collection is of repositories for the same | ||
472 | 425 | # project. | ||
473 | 426 | project = self.factory.makeProduct() | ||
474 | 427 | repositories = [ | ||
475 | 428 | self.factory.makeGitRepository(target=project) for _ in range(3)] | ||
476 | 429 | self.factory.makeGitRepository(target=self.factory.makeProduct()) | ||
477 | 430 | self.factory.makeGitRepository( | ||
478 | 431 | owner=repositories[0].owner, target=repositories[0].owner) | ||
479 | 432 | self.factory.makeGitRepository( | ||
480 | 433 | target=self.factory.makeDistributionSourcePackage()) | ||
481 | 434 | self.assertContentEqual( | ||
482 | 435 | repositories, | ||
483 | 436 | repositories[0].namespace.collection.getRepositories()) | ||
484 | 437 | |||
485 | 315 | 438 | ||
486 | 316 | class TestProjectGitNamespacePrivacyWithInformationType(TestCaseWithFactory): | 439 | class TestProjectGitNamespacePrivacyWithInformationType(TestCaseWithFactory): |
487 | 317 | """Tests for the privacy aspects of `ProjectGitNamespace`. | 440 | """Tests for the privacy aspects of `ProjectGitNamespace`. |
488 | @@ -521,6 +644,65 @@ | |||
489 | 521 | namespace = PackageGitNamespace(person, dsp) | 644 | namespace = PackageGitNamespace(person, dsp) |
490 | 522 | self.assertEqual(dsp, namespace.target) | 645 | self.assertEqual(dsp, namespace.target) |
491 | 523 | 646 | ||
492 | 647 | def test_supports_merge_proposals(self): | ||
493 | 648 | # Package namespaces support merge proposals. | ||
494 | 649 | self.assertTrue(self.getNamespace().supports_merge_proposals) | ||
495 | 650 | |||
496 | 651 | def test_areRepositoriesMergeable_same_repository(self): | ||
497 | 652 | # A package repository is mergeable into itself. | ||
498 | 653 | dsp = self.factory.makeDistributionSourcePackage() | ||
499 | 654 | repository = self.factory.makeGitRepository(target=dsp) | ||
500 | 655 | self.assertTrue( | ||
501 | 656 | repository.namespace.areRepositoriesMergeable( | ||
502 | 657 | repository, repository)) | ||
503 | 658 | |||
504 | 659 | def test_areRepositoriesMergeable_same_namespace(self): | ||
505 | 660 | # Repositories of the same package are mergeable. | ||
506 | 661 | dsp = self.factory.makeDistributionSourcePackage() | ||
507 | 662 | this = self.factory.makeGitRepository(target=dsp) | ||
508 | 663 | other = self.factory.makeGitRepository(target=dsp) | ||
509 | 664 | self.assertTrue(this.namespace.areRepositoriesMergeable(this, other)) | ||
510 | 665 | |||
511 | 666 | def test_areRepositoriesMergeable_different_namespace(self): | ||
512 | 667 | # Repositories of a different package are not mergeable. | ||
513 | 668 | this_dsp = self.factory.makeDistributionSourcePackage() | ||
514 | 669 | this = self.factory.makeGitRepository(target=this_dsp) | ||
515 | 670 | other_dsp = self.factory.makeDistributionSourcePackage() | ||
516 | 671 | other = self.factory.makeGitRepository(target=other_dsp) | ||
517 | 672 | self.assertFalse(this.namespace.areRepositoriesMergeable(this, other)) | ||
518 | 673 | |||
519 | 674 | def test_areRepositoriesMergeable_personal(self): | ||
520 | 675 | # Personal repositories are not mergeable into package repositories. | ||
521 | 676 | owner = self.factory.makePerson() | ||
522 | 677 | dsp = self.factory.makeProduct() | ||
523 | 678 | this = self.factory.makeGitRepository(owner=owner, target=dsp) | ||
524 | 679 | other = self.factory.makeGitRepository(owner=owner, target=owner) | ||
525 | 680 | self.assertFalse(this.namespace.areRepositoriesMergeable(this, other)) | ||
526 | 681 | |||
527 | 682 | def test_areRepositoriesMergeable_project(self): | ||
528 | 683 | # Project repositories are not mergeable into package repositories. | ||
529 | 684 | owner = self.factory.makePerson() | ||
530 | 685 | dsp = self.factory.makeDistributionSourcePackage() | ||
531 | 686 | this = self.factory.makeGitRepository(owner=owner, target=dsp) | ||
532 | 687 | project = self.factory.makeProduct() | ||
533 | 688 | other = self.factory.makeGitRepository(owner=owner, target=project) | ||
534 | 689 | self.assertFalse(this.namespace.areRepositoriesMergeable(this, other)) | ||
535 | 690 | |||
536 | 691 | def test_collection(self): | ||
537 | 692 | # A package namespace's collection is of repositories for the same | ||
538 | 693 | # package. | ||
539 | 694 | dsp = self.factory.makeDistributionSourcePackage() | ||
540 | 695 | repositories = [ | ||
541 | 696 | self.factory.makeGitRepository(target=dsp) for _ in range(3)] | ||
542 | 697 | self.factory.makeGitRepository( | ||
543 | 698 | target=self.factory.makeDistributionSourcePackage()) | ||
544 | 699 | self.factory.makeGitRepository(target=self.factory.makeProduct()) | ||
545 | 700 | self.factory.makeGitRepository( | ||
546 | 701 | owner=repositories[0].owner, target=repositories[0].owner) | ||
547 | 702 | self.assertContentEqual( | ||
548 | 703 | repositories, | ||
549 | 704 | repositories[0].namespace.collection.getRepositories()) | ||
550 | 705 | |||
551 | 524 | 706 | ||
552 | 525 | class BaseCanCreateRepositoriesMixin: | 707 | class BaseCanCreateRepositoriesMixin: |
553 | 526 | """Common tests for all namespaces.""" | 708 | """Common tests for all namespaces.""" |
554 | 527 | 709 | ||
555 | === modified file 'lib/lp/code/model/tests/test_gitref.py' | |||
556 | --- lib/lp/code/model/tests/test_gitref.py 2017-10-04 01:29:35 +0000 | |||
557 | +++ lib/lp/code/model/tests/test_gitref.py 2018-07-11 08:48:06 +0000 | |||
558 | @@ -301,14 +301,25 @@ | |||
559 | 301 | else: | 301 | else: |
560 | 302 | self.assertEqual(review_type, vote.review_type) | 302 | self.assertEqual(review_type, vote.review_type) |
561 | 303 | 303 | ||
564 | 304 | def test_personal_source(self): | 304 | def test_personal_source_project_target(self): |
565 | 305 | """Personal repositories cannot be used as a source for MPs.""" | 305 | """Personal repositories cannot be used as a source for MPs to |
566 | 306 | project repositories. | ||
567 | 307 | """ | ||
568 | 306 | self.source.repository.setTarget( | 308 | self.source.repository.setTarget( |
569 | 307 | target=self.source.owner, user=self.source.owner) | 309 | target=self.source.owner, user=self.source.owner) |
570 | 308 | self.assertRaises( | 310 | self.assertRaises( |
571 | 309 | InvalidBranchMergeProposal, self.source.addLandingTarget, | 311 | InvalidBranchMergeProposal, self.source.addLandingTarget, |
572 | 310 | self.user, self.target) | 312 | self.user, self.target) |
573 | 311 | 313 | ||
574 | 314 | def test_personal_source_personal_target(self): | ||
575 | 315 | """A branch in a personal repository can be used as a source for MPs | ||
576 | 316 | to another branch in the same personal repository. | ||
577 | 317 | """ | ||
578 | 318 | self.target.repository.setTarget( | ||
579 | 319 | target=self.target.owner, user=self.target.owner) | ||
580 | 320 | [source] = self.factory.makeGitRefs(repository=self.target.repository) | ||
581 | 321 | source.addLandingTarget(self.user, self.target) | ||
582 | 322 | |||
583 | 312 | def test_target_repository_same_target(self): | 323 | def test_target_repository_same_target(self): |
584 | 313 | """The target repository's target must match that of the source.""" | 324 | """The target repository's target must match that of the source.""" |
585 | 314 | self.target.repository.setTarget( | 325 | self.target.repository.setTarget( |
Why is the suggestion widget now sometimes called on a GitRef rather than a GitRepository?