Merge lp:~cjwatson/launchpad/relax-personal-git-mp-restrictions into lp:launchpad

Proposed by Colin Watson
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
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.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) wrote :

Why is the suggestion widget now sometimes called on a GitRef rather than a GitRepository?

review: Approve (code)
Revision history for this message
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:+register-merge. It happened to work by chance because GitRef has lots of properties that pass through to the repository. I only noticed when I tried to set `target_repositories = [repository]` in the personal namespace case and found that the UI rendered it as something like "~owner/+git/repo:branch" rather than just "~owner/+git/repo".

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:+register-merge.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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 @@
5-# Copyright 2009-2016 Canonical Ltd. This software is licensed under the
6+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8
9 """Widgets related to IBranch."""
10@@ -34,7 +34,9 @@
11
12 from lp.app.widgets.itemswidgets import LaunchpadRadioWidget
13 from lp.code.interfaces.gitcollection import IGitCollection
14+from lp.code.interfaces.gitref import IGitRef
15 from lp.code.interfaces.gitrepository import IGitRepositorySet
16+from lp.registry.interfaces.person import IPerson
17 from lp.services.webapp import canonical_url
18 from lp.services.webapp.escaping import (
19 html_escape,
20@@ -295,31 +297,42 @@
21 """
22
23 @staticmethod
24- def _generateSuggestionVocab(repository, full_vocabulary):
25+ def _generateSuggestionVocab(context, full_vocabulary):
26 """Generate the vocabulary for the radio buttons.
27
28 The generated vocabulary contains the default repository for the
29 target if there is one, and also any other repositories that the
30 user has specified recently as a target for a proposed merge.
31 """
32- default_target = getUtility(IGitRepositorySet).getDefaultRepository(
33- repository.target)
34- logged_in_user = getUtility(ILaunchBag).user
35- since = datetime.now(utc) - timedelta(days=90)
36- collection = IGitCollection(repository.target).targetedBy(
37- logged_in_user, since)
38- collection = collection.visibleByUser(logged_in_user)
39- # May actually need some eager loading, but the API isn't fine grained
40- # yet.
41- repositories = collection.getRepositories(eager_load=False).config(
42- distinct=True)
43- target_repositories = list(repositories.config(limit=5))
44- # If there is a default repository, make sure it is always shown,
45- # and as the first item.
46- if default_target is not None:
47- if default_target in target_repositories:
48- target_repositories.remove(default_target)
49- target_repositories.insert(0, default_target)
50+ if IGitRef.providedBy(context):
51+ repository = context.repository
52+ else:
53+ repository = context
54+
55+ if IPerson.providedBy(repository.target):
56+ # If the source is a personal repository, then the only valid
57+ # target is that same repository.
58+ target_repositories = [repository]
59+ else:
60+ repository_set = getUtility(IGitRepositorySet)
61+ default_target = repository_set.getDefaultRepository(
62+ repository.target)
63+ logged_in_user = getUtility(ILaunchBag).user
64+ since = datetime.now(utc) - timedelta(days=90)
65+ collection = IGitCollection(repository.target).targetedBy(
66+ logged_in_user, since)
67+ collection = collection.visibleByUser(logged_in_user)
68+ # May actually need some eager loading, but the API isn't fine
69+ # grained yet.
70+ repositories = collection.getRepositories(eager_load=False).config(
71+ distinct=True)
72+ target_repositories = list(repositories.config(limit=5))
73+ # If there is a default repository, make sure it is always
74+ # shown, and as the first item.
75+ if default_target is not None:
76+ if default_target in target_repositories:
77+ target_repositories.remove(default_target)
78+ target_repositories.insert(0, default_target)
79
80 terms = []
81 for repository in target_repositories:
82@@ -335,10 +348,12 @@
83 # instead to have a separate link to the repository details.
84 text = u'%s (<a href="%s">repository details</a>)'
85 # If the repository is the default for the target, say so.
86- default_target = getUtility(IGitRepositorySet).getDefaultRepository(
87- repository.target)
88- if repository == default_target:
89- text += u"&ndash; <em>default repository</em>"
90+ if not IPerson.providedBy(repository.target):
91+ repository_set = getUtility(IGitRepositorySet)
92+ default_target = repository_set.getDefaultRepository(
93+ repository.target)
94+ if repository == default_target:
95+ text += u"&ndash; <em>default repository</em>"
96 label = (
97 u'<label for="%s" style="font-weight: normal">' + text +
98 u'</label>')
99
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 @@
104-# Copyright 2011-2015 Canonical Ltd. This software is licensed under the
105+# Copyright 2011-2018 Canonical Ltd. This software is licensed under the
106 # GNU Affero General Public License version 3 (see the file LICENSE).
107
108 __metaclass__ = type
109@@ -199,11 +199,11 @@
110 DocTestMatches(expected, self.doctest_opts))
111
112
113-def make_target_git_repository_widget(repository):
114- """Given a Git repository, return a widget for selecting where to land
115- it."""
116+def make_target_git_repository_widget(context):
117+ """Given a Git repository or reference, return a widget for selecting
118+ where to land it."""
119 choice = Choice(__name__='test_field', vocabulary='GitRepository')
120- choice = choice.bind(repository)
121+ choice = choice.bind(context)
122 request = LaunchpadTestRequest()
123 return TargetGitRepositoryWidget(choice, None, request)
124
125@@ -216,41 +216,79 @@
126 doctest.NORMALIZE_WHITESPACE | doctest.REPORT_NDIFF |
127 doctest.ELLIPSIS)
128
129- def makeRepositoryAndOldMergeProposal(self, timedelta):
130- """Make an old merge proposal and a repository with the same target."""
131+ def makeRefAndOldMergeProposal(self, timedelta):
132+ """Make an old merge proposal and a ref with the same target."""
133 bmp = self.factory.makeBranchMergeProposalForGit(
134 date_created=datetime.now(utc) - timedelta)
135 login_person(bmp.registrant)
136- target = bmp.target_git_repository
137- return target, self.factory.makeGitRepository(target=target.target)
138+ target = bmp.merge_target
139+ return target, self.factory.makeGitRefs(target=target.target)[0]
140
141 def test_recent_target(self):
142 """Targets for proposals newer than 90 days are included."""
143- target, source = self.makeRepositoryAndOldMergeProposal(
144- timedelta(days=89))
145+ target, source = self.makeRefAndOldMergeProposal(timedelta(days=89))
146 widget = make_target_git_repository_widget(source)
147- self.assertIn(target, widget.suggestion_vocab)
148+ self.assertIn(target.repository, widget.suggestion_vocab)
149
150 def test_stale_target(self):
151 """Targets for proposals older than 90 days are not considered."""
152- target, source = self.makeRepositoryAndOldMergeProposal(
153- timedelta(days=91))
154- widget = make_target_git_repository_widget(source)
155- self.assertNotIn(target, widget.suggestion_vocab)
156+ target, source = self.makeRefAndOldMergeProposal(timedelta(days=91))
157+ widget = make_target_git_repository_widget(source)
158+ self.assertNotIn(target.repository, widget.suggestion_vocab)
159+
160+ def test_personal_repository(self):
161+ """Proposals for personal repositories only suggest that repository."""
162+ owner = self.factory.makePerson()
163+ this_source, this_target = self.factory.makeGitRefs(
164+ owner=owner, target=owner,
165+ paths=[u"refs/heads/source", u"refs/heads/target"])
166+ bmp = self.factory.makeBranchMergeProposalForGit(
167+ source_ref=this_source, target_ref=this_target,
168+ date_created=datetime.now(utc) - timedelta(days=1))
169+ other_source, other_target = self.factory.makeGitRefs(
170+ owner=owner, target=owner,
171+ paths=[u"refs/heads/source", u"refs/heads/target"])
172+ self.factory.makeBranchMergeProposalForGit(
173+ source_ref=other_source, target_ref=other_target,
174+ date_created=datetime.now(utc) - timedelta(days=1))
175+ login_person(bmp.registrant)
176+ [source] = self.factory.makeGitRefs(repository=this_target.repository)
177+ widget = make_target_git_repository_widget(source)
178+ self.assertContentEqual(
179+ [this_target.repository],
180+ [term.value for term in widget.suggestion_vocab])
181
182 def test__renderSuggestionLabel(self):
183 """Git repositories have a reasonable suggestion label."""
184- target, source = self.makeRepositoryAndOldMergeProposal(
185- timedelta(days=1))
186+ target, source = self.makeRefAndOldMergeProposal(timedelta(days=1))
187 login_person(target.target.owner)
188 getUtility(IGitRepositorySet).setDefaultRepository(
189- target.target, target)
190+ target.target, target.repository)
191 widget = make_target_git_repository_widget(source)
192 expected = (
193 """<label for="field.test_field.2"
194 ...>... (<a href="...">repository details</a>)&ndash;
195 <em>default repository</em></label>""")
196- structured_string = widget._renderSuggestionLabel(target, 2)
197+ structured_string = widget._renderSuggestionLabel(target.repository, 2)
198+ self.assertThat(
199+ structured_string.escapedtext,
200+ DocTestMatches(expected, self.doctest_opts))
201+
202+ def test__renderSuggestionLabel_personal(self):
203+ """Personal Git repositories have a reasonable suggestion label."""
204+ owner = self.factory.makePerson()
205+ source, target = self.factory.makeGitRefs(
206+ owner=owner, target=owner,
207+ paths=[u"refs/heads/source", u"refs/heads/target"])
208+ bmp = self.factory.makeBranchMergeProposalForGit(
209+ source_ref=source, target_ref=target,
210+ date_created=datetime.now(utc) - timedelta(days=1))
211+ login_person(bmp.registrant)
212+ widget = make_target_git_repository_widget(source)
213+ expected = (
214+ """<label for="field.test_field.2"
215+ ...>... (<a href="...">repository details</a>)</label>""")
216+ structured_string = widget._renderSuggestionLabel(target.repository, 2)
217 self.assertThat(
218 structured_string.escapedtext,
219 DocTestMatches(expected, self.doctest_opts))
220
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 @@
225-# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
226+# Copyright 2015-2018 Canonical Ltd. This software is licensed under the
227 # GNU Affero General Public License version 3 (see the file LICENSE).
228
229 """Interface for a Git repository namespace."""
230@@ -178,8 +178,12 @@
231 already exists in the namespace.
232 """
233
234- def areRepositoriesMergeable(other_namespace):
235- """Are repositories from other_namespace mergeable into this one?"""
236+ def areRepositoriesMergeable(this, other):
237+ """Is `other` mergeable into `this`?
238+
239+ :param this: An `IGitRepository` in this namespace.
240+ :param other: An `IGitRepository` in either this or another namespace.
241+ """
242
243 collection = Attribute("An `IGitCollection` for this namespace.")
244
245
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 @@
250-# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
251+# Copyright 2015-2018 Canonical Ltd. This software is licensed under the
252 # GNU Affero General Public License version 3 (see the file LICENSE).
253
254 """Implementations of `IGitNamespace`."""
255@@ -250,7 +250,7 @@
256
257 has_defaults = False
258 allow_push_to_set_default = False
259- supports_merge_proposals = False
260+ supports_merge_proposals = True
261 supports_code_imports = False
262 allow_recipe_name_from_target = False
263
264@@ -303,15 +303,17 @@
265 else:
266 return InformationType.PUBLIC
267
268- def areRepositoriesMergeable(self, other_namespace):
269+ def areRepositoriesMergeable(self, this, other):
270 """See `IGitNamespacePolicy`."""
271- return False
272+ if this.namespace != self:
273+ raise AssertionError(
274+ "Namespace of %s is not %s." % (this.unique_name, self.name))
275+ return this == other
276
277 @property
278 def collection(self):
279 """See `IGitNamespacePolicy`."""
280- return getUtility(IAllGitRepositories).ownedBy(
281- self.person).isPersonal()
282+ return getUtility(IAllGitRepositories).ownedBy(self.owner).isPersonal()
283
284 def assignKarma(self, person, action_name, date_created=None):
285 """See `IGitNamespacePolicy`."""
286@@ -383,12 +385,16 @@
287 return None
288 return default_type
289
290- def areRepositoriesMergeable(self, other_namespace):
291+ def areRepositoriesMergeable(self, this, other):
292 """See `IGitNamespacePolicy`."""
293 # Repositories are mergeable into a project repository if the
294 # project is the same.
295 # XXX cjwatson 2015-04-18: Allow merging from a package repository
296 # if any (active?) series is linked to this project.
297+ if this.namespace != self:
298+ raise AssertionError(
299+ "Namespace of %s is not %s." % (this.unique_name, self.name))
300+ other_namespace = other.namespace
301 if zope_isinstance(other_namespace, ProjectGitNamespace):
302 return self.target == other_namespace.target
303 else:
304@@ -457,12 +463,16 @@
305 """See `IGitNamespace`."""
306 return InformationType.PUBLIC
307
308- def areRepositoriesMergeable(self, other_namespace):
309+ def areRepositoriesMergeable(self, this, other):
310 """See `IGitNamespacePolicy`."""
311 # Repositories are mergeable into a package repository if the
312 # package is the same.
313 # XXX cjwatson 2015-04-18: Allow merging from a project repository
314 # if any (active?) series links this package to that project.
315+ if this.namespace != self:
316+ raise AssertionError(
317+ "Namespace of %s is not %s." % (this.unique_name, self.name))
318+ other_namespace = other.namespace
319 if zope_isinstance(other_namespace, PackageGitNamespace):
320 return self.target == other_namespace.target
321 else:
322
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 @@
327-# Copyright 2015-2017 Canonical Ltd. This software is licensed under the
328+# Copyright 2015-2018 Canonical Ltd. This software is licensed under the
329 # GNU Affero General Public License version 3 (see the file LICENSE).
330
331 __metaclass__ = type
332@@ -946,7 +946,7 @@
333
334 def isRepositoryMergeable(self, other):
335 """See `IGitRepository`."""
336- return self.namespace.areRepositoriesMergeable(other.namespace)
337+ return self.namespace.areRepositoriesMergeable(self, other)
338
339 @property
340 def pending_updates(self):
341
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 @@
346-# Copyright 2015-2017 Canonical Ltd. This software is licensed under the
347+# Copyright 2015-2018 Canonical Ltd. This software is licensed under the
348 # GNU Affero General Public License version 3 (see the file LICENSE).
349
350 """Tests for `IGitNamespace` implementations."""
351@@ -278,6 +278,70 @@
352 namespace = PersonalGitNamespace(person)
353 self.assertEqual(person, namespace.target)
354
355+ def test_supports_merge_proposals(self):
356+ # Personal namespaces support merge proposals.
357+ self.assertTrue(self.getNamespace().supports_merge_proposals)
358+
359+ def test_areRepositoriesMergeable_same_repository(self):
360+ # A personal repository is mergeable into itself.
361+ owner = self.factory.makePerson()
362+ repository = self.factory.makeGitRepository(owner=owner, target=owner)
363+ self.assertTrue(
364+ repository.namespace.areRepositoriesMergeable(
365+ repository, repository))
366+
367+ def test_areRepositoriesMergeable_same_namespace(self):
368+ # A personal repository is not mergeable into another personal
369+ # repository, even if they are in the same namespace.
370+ owner = self.factory.makePerson()
371+ this = self.factory.makeGitRepository(owner=owner, target=owner)
372+ other = self.factory.makeGitRepository(owner=owner, target=owner)
373+ self.assertFalse(this.namespace.areRepositoriesMergeable(this, other))
374+
375+ def test_areRepositoriesMergeable_different_namespace(self):
376+ # A personal repository is not mergeable into another personal
377+ # repository with a different namespace.
378+ this_owner = self.factory.makePerson()
379+ this = self.factory.makeGitRepository(
380+ owner=this_owner, target=this_owner)
381+ other_owner = self.factory.makePerson()
382+ other = self.factory.makeGitRepository(
383+ owner=other_owner, target=other_owner)
384+ self.assertFalse(this.namespace.areRepositoriesMergeable(this, other))
385+
386+ def test_areRepositoriesMergeable_project(self):
387+ # Project repositories are not mergeable into personal repositories.
388+ owner = self.factory.makePerson()
389+ this = self.factory.makeGitRepository(owner=owner, target=owner)
390+ project = self.factory.makeProduct()
391+ other = self.factory.makeGitRepository(owner=owner, target=project)
392+ self.assertFalse(this.namespace.areRepositoriesMergeable(this, other))
393+
394+ def test_areRepositoriesMergeable_package(self):
395+ # Package repositories are not mergeable into personal repositories.
396+ owner = self.factory.makePerson()
397+ this = self.factory.makeGitRepository(owner=owner, target=owner)
398+ dsp = self.factory.makeDistributionSourcePackage()
399+ other = self.factory.makeGitRepository(owner=owner, target=dsp)
400+ self.assertFalse(this.namespace.areRepositoriesMergeable(this, other))
401+
402+ def test_collection(self):
403+ # A personal namespace's collection is of personal repositories with
404+ # the same owner.
405+ owner = self.factory.makePerson()
406+ repositories = [
407+ self.factory.makeGitRepository(owner=owner, target=owner)
408+ for _ in range(3)]
409+ other_owner = self.factory.makePerson()
410+ self.factory.makeGitRepository(owner=other_owner, target=other_owner)
411+ self.factory.makeGitRepository(
412+ owner=owner, target=self.factory.makeProduct())
413+ self.factory.makeGitRepository(
414+ owner=owner, target=self.factory.makeDistributionSourcePackage())
415+ self.assertContentEqual(
416+ repositories,
417+ repositories[0].namespace.collection.getRepositories())
418+
419
420 class TestProjectGitNamespace(TestCaseWithFactory, NamespaceMixin):
421 """Tests for `ProjectGitNamespace`."""
422@@ -312,6 +376,65 @@
423 namespace = ProjectGitNamespace(person, project)
424 self.assertEqual(project, namespace.target)
425
426+ def test_supports_merge_proposals(self):
427+ # Project namespaces support merge proposals.
428+ self.assertTrue(self.getNamespace().supports_merge_proposals)
429+
430+ def test_areRepositoriesMergeable_same_repository(self):
431+ # A project repository is mergeable into itself.
432+ project = self.factory.makeProduct()
433+ repository = self.factory.makeGitRepository(target=project)
434+ self.assertTrue(
435+ repository.namespace.areRepositoriesMergeable(
436+ repository, repository))
437+
438+ def test_areRepositoriesMergeable_same_namespace(self):
439+ # Repositories of the same project are mergeable.
440+ project = self.factory.makeProduct()
441+ this = self.factory.makeGitRepository(target=project)
442+ other = self.factory.makeGitRepository(target=project)
443+ self.assertTrue(this.namespace.areRepositoriesMergeable(this, other))
444+
445+ def test_areRepositoriesMergeable_different_namespace(self):
446+ # Repositories of a different project are not mergeable.
447+ this_project = self.factory.makeProduct()
448+ this = self.factory.makeGitRepository(target=this_project)
449+ other_project = self.factory.makeProduct()
450+ other = self.factory.makeGitRepository(target=other_project)
451+ self.assertFalse(this.namespace.areRepositoriesMergeable(this, other))
452+
453+ def test_areRepositoriesMergeable_personal(self):
454+ # Personal repositories are not mergeable into project repositories.
455+ owner = self.factory.makePerson()
456+ project = self.factory.makeProduct()
457+ this = self.factory.makeGitRepository(owner=owner, target=project)
458+ other = self.factory.makeGitRepository(owner=owner, target=owner)
459+ self.assertFalse(this.namespace.areRepositoriesMergeable(this, other))
460+
461+ def test_areRepositoriesMergeable_package(self):
462+ # Package repositories are not mergeable into project repositories.
463+ owner = self.factory.makePerson()
464+ project = self.factory.makeProduct()
465+ this = self.factory.makeGitRepository(owner=owner, target=project)
466+ dsp = self.factory.makeDistributionSourcePackage()
467+ other = self.factory.makeGitRepository(owner=owner, target=dsp)
468+ self.assertFalse(this.namespace.areRepositoriesMergeable(this, other))
469+
470+ def test_collection(self):
471+ # A project namespace's collection is of repositories for the same
472+ # project.
473+ project = self.factory.makeProduct()
474+ repositories = [
475+ self.factory.makeGitRepository(target=project) for _ in range(3)]
476+ self.factory.makeGitRepository(target=self.factory.makeProduct())
477+ self.factory.makeGitRepository(
478+ owner=repositories[0].owner, target=repositories[0].owner)
479+ self.factory.makeGitRepository(
480+ target=self.factory.makeDistributionSourcePackage())
481+ self.assertContentEqual(
482+ repositories,
483+ repositories[0].namespace.collection.getRepositories())
484+
485
486 class TestProjectGitNamespacePrivacyWithInformationType(TestCaseWithFactory):
487 """Tests for the privacy aspects of `ProjectGitNamespace`.
488@@ -521,6 +644,65 @@
489 namespace = PackageGitNamespace(person, dsp)
490 self.assertEqual(dsp, namespace.target)
491
492+ def test_supports_merge_proposals(self):
493+ # Package namespaces support merge proposals.
494+ self.assertTrue(self.getNamespace().supports_merge_proposals)
495+
496+ def test_areRepositoriesMergeable_same_repository(self):
497+ # A package repository is mergeable into itself.
498+ dsp = self.factory.makeDistributionSourcePackage()
499+ repository = self.factory.makeGitRepository(target=dsp)
500+ self.assertTrue(
501+ repository.namespace.areRepositoriesMergeable(
502+ repository, repository))
503+
504+ def test_areRepositoriesMergeable_same_namespace(self):
505+ # Repositories of the same package are mergeable.
506+ dsp = self.factory.makeDistributionSourcePackage()
507+ this = self.factory.makeGitRepository(target=dsp)
508+ other = self.factory.makeGitRepository(target=dsp)
509+ self.assertTrue(this.namespace.areRepositoriesMergeable(this, other))
510+
511+ def test_areRepositoriesMergeable_different_namespace(self):
512+ # Repositories of a different package are not mergeable.
513+ this_dsp = self.factory.makeDistributionSourcePackage()
514+ this = self.factory.makeGitRepository(target=this_dsp)
515+ other_dsp = self.factory.makeDistributionSourcePackage()
516+ other = self.factory.makeGitRepository(target=other_dsp)
517+ self.assertFalse(this.namespace.areRepositoriesMergeable(this, other))
518+
519+ def test_areRepositoriesMergeable_personal(self):
520+ # Personal repositories are not mergeable into package repositories.
521+ owner = self.factory.makePerson()
522+ dsp = self.factory.makeProduct()
523+ this = self.factory.makeGitRepository(owner=owner, target=dsp)
524+ other = self.factory.makeGitRepository(owner=owner, target=owner)
525+ self.assertFalse(this.namespace.areRepositoriesMergeable(this, other))
526+
527+ def test_areRepositoriesMergeable_project(self):
528+ # Project repositories are not mergeable into package repositories.
529+ owner = self.factory.makePerson()
530+ dsp = self.factory.makeDistributionSourcePackage()
531+ this = self.factory.makeGitRepository(owner=owner, target=dsp)
532+ project = self.factory.makeProduct()
533+ other = self.factory.makeGitRepository(owner=owner, target=project)
534+ self.assertFalse(this.namespace.areRepositoriesMergeable(this, other))
535+
536+ def test_collection(self):
537+ # A package namespace's collection is of repositories for the same
538+ # package.
539+ dsp = self.factory.makeDistributionSourcePackage()
540+ repositories = [
541+ self.factory.makeGitRepository(target=dsp) for _ in range(3)]
542+ self.factory.makeGitRepository(
543+ target=self.factory.makeDistributionSourcePackage())
544+ self.factory.makeGitRepository(target=self.factory.makeProduct())
545+ self.factory.makeGitRepository(
546+ owner=repositories[0].owner, target=repositories[0].owner)
547+ self.assertContentEqual(
548+ repositories,
549+ repositories[0].namespace.collection.getRepositories())
550+
551
552 class BaseCanCreateRepositoriesMixin:
553 """Common tests for all namespaces."""
554
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 else:
560 self.assertEqual(review_type, vote.review_type)
561
562- def test_personal_source(self):
563- """Personal repositories cannot be used as a source for MPs."""
564+ def test_personal_source_project_target(self):
565+ """Personal repositories cannot be used as a source for MPs to
566+ project repositories.
567+ """
568 self.source.repository.setTarget(
569 target=self.source.owner, user=self.source.owner)
570 self.assertRaises(
571 InvalidBranchMergeProposal, self.source.addLandingTarget,
572 self.user, self.target)
573
574+ def test_personal_source_personal_target(self):
575+ """A branch in a personal repository can be used as a source for MPs
576+ to another branch in the same personal repository.
577+ """
578+ self.target.repository.setTarget(
579+ target=self.target.owner, user=self.target.owner)
580+ [source] = self.factory.makeGitRefs(repository=self.target.repository)
581+ source.addLandingTarget(self.user, self.target)
582+
583 def test_target_repository_same_target(self):
584 """The target repository's target must match that of the source."""
585 self.target.repository.setTarget(