launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #18476
[Merge] lp:~cjwatson/launchpad/git-default-owner into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/git-default-owner into lp:launchpad.
Commit message:
Allow any member of a team to set an owner-target default; use this to set the owner of newly-pushed target defaults to the target owner.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/git-default-owner/+merge/258400
When a project is owned by a team and a team member pushes a new repository to the project default (/PROJECT), the unique name of the repository ends up as /~REQUESTER/PROJECT/+git/PROJECT. This is weird because other members of the project-owning team can't write to it. A more reasonable default would be /~OWNER/PROJECT/+git/PROJECT, which requires opening up the permissions on IGitRepositorySet.setDefaultRepositoryForOwner a little bit to allow non-owner members of a team to call it.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/git-default-owner into lp:launchpad.
=== modified file 'lib/lp/code/interfaces/gitrepository.py'
--- lib/lp/code/interfaces/gitrepository.py 2015-04-28 16:39:15 +0000
+++ lib/lp/code/interfaces/gitrepository.py 2015-05-06 15:07:21 +0000
@@ -690,6 +690,7 @@
:raises GitTargetError: if `target` is an `IPerson`.
"""
+ @call_with(user=REQUEST_USER)
@operation_parameters(
owner=Reference(title=_("Owner"), required=True, schema=IPerson),
target=Reference(
@@ -698,13 +699,14 @@
title=_("Git repository"), required=False, schema=IGitRepository))
@export_write_operation()
@operation_for_version("devel")
- def setDefaultRepositoryForOwner(owner, target, repository):
+ def setDefaultRepositoryForOwner(owner, target, repository, user):
"""Set a person's default repository for a target.
:param owner: An `IPerson`.
:param target: An `IHasGitRepositories`.
:param repository: An `IGitRepository`, or None to unset the default
repository.
+ :param user: The `IPerson` who is making the change.
:raises GitTargetError: if `target` is an `IPerson`.
"""
=== modified file 'lib/lp/code/model/gitrepository.py'
--- lib/lp/code/model/gitrepository.py 2015-04-28 16:39:15 +0000
+++ lib/lp/code/model/gitrepository.py 2015-05-06 15:07:21 +0000
@@ -845,9 +845,17 @@
if previous is not None:
previous.setTargetDefault(False)
- @available_with_permission('launchpad.Edit', 'owner')
- def setDefaultRepositoryForOwner(self, owner, target, repository):
+ def setDefaultRepositoryForOwner(self, owner, target, repository, user):
"""See `IGitRepositorySet`."""
+ if not user.inTeam(owner):
+ if owner.is_team:
+ raise Unauthorized(
+ "%s is not a member of %s" %
+ (user.displayname, owner.displayname))
+ else:
+ raise Unauthorized(
+ "%s cannot set a default Git repository for %s" %
+ (user.displayname, owner.displayname))
if IPerson.providedBy(target):
raise GitTargetError(
"Cannot set a default Git repository for a person, only "
=== modified file 'lib/lp/code/model/tests/test_gitrepository.py'
--- lib/lp/code/model/tests/test_gitrepository.py 2015-04-28 16:39:15 +0000
+++ lib/lp/code/model/tests/test_gitrepository.py 2015-05-06 15:07:21 +0000
@@ -217,9 +217,9 @@
# identity is a combination of the person and project names.
project = self.factory.makeProduct()
repository = self.factory.makeGitRepository(target=project)
- with person_logged_in(repository.owner):
+ with person_logged_in(repository.owner) as user:
self.repository_set.setDefaultRepositoryForOwner(
- repository.owner, project, repository)
+ repository.owner, project, repository, user)
self.assertGitIdentity(
repository, "~%s/%s" % (repository.owner.name, project.name))
@@ -228,9 +228,9 @@
# identity is a combination of the person name and the package path.
dsp = self.factory.makeDistributionSourcePackage()
repository = self.factory.makeGitRepository(target=dsp)
- with person_logged_in(repository.owner):
+ with person_logged_in(repository.owner) as user:
self.repository_set.setDefaultRepositoryForOwner(
- repository.owner, dsp, repository)
+ repository.owner, dsp, repository, user)
self.assertGitIdentity(
repository,
"~%s/%s/+source/%s" % (
@@ -253,9 +253,9 @@
fooix = self.factory.makeProduct(name="fooix", owner=eric)
repository = self.factory.makeGitRepository(
owner=eric, target=fooix, name=u"fooix-repo")
- with person_logged_in(fooix.owner):
+ with person_logged_in(fooix.owner) as user:
self.repository_set.setDefaultRepositoryForOwner(
- repository.owner, fooix, repository)
+ repository.owner, fooix, repository, user)
self.repository_set.setDefaultRepository(fooix, repository)
eric_fooix = getUtility(IPersonProductFactory).create(eric, fooix)
self.assertEqual(
@@ -280,7 +280,7 @@
dsp = repository.target
with admin_logged_in():
self.repository_set.setDefaultRepositoryForOwner(
- repository.owner, dsp, repository)
+ repository.owner, dsp, repository, repository.owner)
self.repository_set.setDefaultRepository(dsp, repository)
eric_dsp = getUtility(IPersonDistributionSourcePackageFactory).create(
eric, dsp)
@@ -1303,11 +1303,11 @@
# setDefaultRepositoryForOwner refuses if the target is a person.
person = self.factory.makePerson()
repository = self.factory.makeGitRepository(owner=person)
- with person_logged_in(person):
+ with person_logged_in(person) as user:
self.assertRaises(
GitTargetError,
self.repository_set.setDefaultRepositoryForOwner,
- person, person, repository)
+ person, person, repository, user)
class TestGitRepositorySetDefaultsMixin:
@@ -1319,7 +1319,8 @@
self.useFixture(FeatureFixture({GIT_FEATURE_FLAG: u"on"}))
self.repository_set = getUtility(IGitRepositorySet)
self.get_method = self.repository_set.getDefaultRepository
- self.set_method = self.repository_set.setDefaultRepository
+ self.set_method = (lambda target, repository, user:
+ self.repository_set.setDefaultRepository(target, repository))
def makeGitRepository(self, target):
return self.factory.makeGitRepository(target=target)
@@ -1330,17 +1331,17 @@
target = self.makeTarget()
repository = self.makeGitRepository(target)
self.assertIsNone(self.get_method(target))
- with person_logged_in(self.getPersonForLogin(target)):
- self.set_method(target, repository)
+ with person_logged_in(self.getPersonForLogin(target)) as user:
+ self.set_method(target, repository, user)
self.assertEqual(repository, self.get_method(target))
def test_set_default_repository_None(self):
# setDefaultRepository*(target, None) clears the default.
target = self.makeTarget()
repository = self.makeGitRepository(target)
- with person_logged_in(self.getPersonForLogin(target)):
- self.set_method(target, repository)
- self.set_method(target, None)
+ with person_logged_in(self.getPersonForLogin(target)) as user:
+ self.set_method(target, repository, user)
+ self.set_method(target, None, user)
self.assertIsNone(self.get_method(target))
def test_set_default_repository_different_target(self):
@@ -1349,9 +1350,9 @@
target = self.makeTarget()
other_target = self.makeTarget(template=target)
repository = self.makeGitRepository(other_target)
- with person_logged_in(self.getPersonForLogin(target)):
+ with person_logged_in(self.getPersonForLogin(target)) as user:
self.assertRaises(
- GitTargetError, self.set_method, target, repository)
+ GitTargetError, self.set_method, target, repository, user)
class TestGitRepositorySetDefaultsProject(
@@ -1396,6 +1397,33 @@
def getPersonForLogin(self, target):
return self.person
+ def test_set_default_repository_for_owner_team_member(self):
+ # A member of the owner team can use setDefaultRepositoryForOwner.
+ target = self.makeTarget()
+ team = self.factory.makeTeam(members=[self.person])
+ repository = self.factory.makeGitRepository(owner=team, target=target)
+ self.assertIsNone(
+ self.repository_set.getDefaultRepositoryForOwner(team, target))
+ with person_logged_in(self.person) as user:
+ self.repository_set.setDefaultRepositoryForOwner(
+ team, target, repository, user)
+ self.assertEqual(
+ repository,
+ self.repository_set.getDefaultRepositoryForOwner(team, target))
+
+ def test_set_default_repository_for_owner_not_team_member(self):
+ # A non-member of the owner team cannot use
+ # setDefaultRepositoryForOwner.
+ target = self.makeTarget()
+ team = self.factory.makeTeam()
+ repository = self.factory.makeGitRepository(owner=team, target=target)
+ self.assertIsNone(
+ self.repository_set.getDefaultRepositoryForOwner(team, target))
+ with person_logged_in(self.person) as user:
+ self.assertRaises(
+ Unauthorized, self.repository_set.setDefaultRepositoryForOwner,
+ team, target, repository, user)
+
class TestGitRepositorySetDefaultsOwnerProject(
TestGitRepositorySetDefaultsOwnerMixin,
=== modified file 'lib/lp/code/xmlrpc/git.py'
--- lib/lp/code/xmlrpc/git.py 2015-05-06 00:31:54 +0000
+++ lib/lp/code/xmlrpc/git.py 2015-05-06 15:07:21 +0000
@@ -113,16 +113,16 @@
# split_git_unique_name should have left us without a repository name.
assert repository is None
if owner is None:
- repository_owner = requester
+ if not get_git_namespace(target, None).allow_push_to_set_default:
+ raise GitRepositoryCreationForbidden(
+ "Cannot automatically set the default repository for this "
+ "target; push to a named repository instead.")
+ repository_owner = target.owner
else:
repository_owner = owner
namespace = get_git_namespace(target, repository_owner)
if repository_name is None and not namespace.has_defaults:
raise InvalidNamespace(path)
- if owner is None and not namespace.allow_push_to_set_default:
- raise GitRepositoryCreationForbidden(
- "Cannot automatically set the default repository for this "
- "target; push to a named repository instead.")
if repository_name is None:
def default_func(new_repository):
if owner is None:
@@ -132,7 +132,7 @@
self.repository_set.getDefaultRepositoryForOwner(
repository_owner, target) is None):
self.repository_set.setDefaultRepositoryForOwner(
- repository_owner, target, new_repository)
+ repository_owner, target, new_repository, requester)
repository_name = namespace.findUnusedName(target.name)
return namespace, repository_name, default_func
=== modified file 'lib/lp/code/xmlrpc/tests/test_git.py'
--- lib/lp/code/xmlrpc/tests/test_git.py 2015-05-06 00:31:54 +0000
+++ lib/lp/code/xmlrpc/tests/test_git.py 2015-05-06 15:07:21 +0000
@@ -22,6 +22,7 @@
IGitRepositorySet,
)
from lp.code.xmlrpc.git import GitAPI
+from lp.registry.enums import TeamMembershipPolicy
from lp.services.features.testing import FeatureFixture
from lp.services.webapp.escaping import html_escape
from lp.testing import (
@@ -288,45 +289,8 @@
requester = self.factory.makePerson()
project = self.factory.makeProduct()
path = u"/%s" % project.name
- message = "You cannot set the default Git repository for '%s'." % (
- path.strip("/"))
- initial_count = getUtility(IAllGitRepositories).count()
- self.assertPermissionDenied(
- requester, path, message=message, permission="write")
- # No repository was created.
- login(ANONYMOUS)
- self.assertEqual(
- initial_count, getUtility(IAllGitRepositories).count())
-
- def test_translatePath_create_project_not_team_owner_default(self):
- # A non-owner member of a team cannot immediately set a
- # newly-created team-owned repository as that team's default for a
- # project.
- requester = self.factory.makePerson()
- team = self.factory.makeTeam(members=[requester])
- project = self.factory.makeProduct()
- path = u"/~%s/%s" % (team.name, project.name)
- message = "You cannot set the default Git repository for '%s'." % (
- path.strip("/"))
- initial_count = getUtility(IAllGitRepositories).count()
- self.assertPermissionDenied(
- requester, path, message=message, permission="write")
- # No repository was created.
- login(ANONYMOUS)
- self.assertEqual(
- initial_count, getUtility(IAllGitRepositories).count())
-
- def test_translatePath_create_package_not_team_owner_default(self):
- # A non-owner member of a team cannot immediately set a
- # newly-created team-owned repository as that team's default for a
- # package.
- requester = self.factory.makePerson()
- team = self.factory.makeTeam(members=[requester])
- dsp = self.factory.makeDistributionSourcePackage()
- path = u"/~%s/%s/+source/%s" % (
- team.name, dsp.distribution.name, dsp.sourcepackagename.name)
- message = "You cannot set the default Git repository for '%s'." % (
- path.strip("/"))
+ message = "%s cannot create Git repositories owned by %s" % (
+ requester.displayname, project.owner.displayname)
initial_count = getUtility(IAllGitRepositories).count()
self.assertPermissionDenied(
requester, path, message=message, permission="write")
@@ -446,9 +410,9 @@
target = self.factory.makeProduct()
repository = self.factory.makeGitRepository(
owner=target.owner, target=target)
- with person_logged_in(target.owner):
+ with person_logged_in(target.owner) as user:
self.repository_set.setDefaultRepositoryForOwner(
- target.owner, target, repository)
+ target.owner, target, repository, user)
self.assertCreatesFromClone(
target.owner,
u"/~%s/%s/+git/random" % (target.owner.name, target.name),
@@ -590,10 +554,14 @@
# A repository can be created and immediately set as the default for
# a project.
requester = self.factory.makePerson()
- project = self.factory.makeProduct(owner=requester)
+ owner = self.factory.makeTeam(
+ membership_policy=TeamMembershipPolicy.RESTRICTED,
+ members=[requester])
+ project = self.factory.makeProduct(owner=owner)
repository = self.assertCreates(requester, u"/%s" % project.name)
self.assertTrue(repository.target_default)
self.assertTrue(repository.owner_default)
+ self.assertEqual(owner, repository.owner)
def test_translatePath_create_package_default_denied(self):
# A repository cannot (yet) be created and immediately set as the
@@ -617,6 +585,7 @@
requester, u"/~%s/%s" % (requester.name, project.name))
self.assertFalse(repository.target_default)
self.assertTrue(repository.owner_default)
+ self.assertEqual(requester, repository.owner)
def test_translatePath_create_project_team_owner_default(self):
# The owner of a team can create a team-owned repository and
@@ -628,6 +597,19 @@
requester, u"/~%s/%s" % (team.name, project.name))
self.assertFalse(repository.target_default)
self.assertTrue(repository.owner_default)
+ self.assertEqual(team, repository.owner)
+
+ def test_translatePath_create_project_team_member_default(self):
+ # A non-owner member of a team can create a team-owned repository
+ # and immediately set it as that team's default for a project.
+ requester = self.factory.makePerson()
+ team = self.factory.makeTeam(members=[requester])
+ project = self.factory.makeProduct()
+ repository = self.assertCreates(
+ requester, u"/~%s/%s" % (team.name, project.name))
+ self.assertFalse(repository.target_default)
+ self.assertTrue(repository.owner_default)
+ self.assertEqual(team, repository.owner)
def test_translatePath_create_package_owner_default(self):
# A repository can be created and immediately set as its owner's
@@ -639,6 +621,7 @@
repository = self.assertCreates(requester, path)
self.assertFalse(repository.target_default)
self.assertTrue(repository.owner_default)
+ self.assertEqual(requester, repository.owner)
def test_translatePath_create_package_team_owner_default(self):
# The owner of a team can create a team-owned repository and
@@ -651,6 +634,20 @@
repository = self.assertCreates(requester, path)
self.assertFalse(repository.target_default)
self.assertTrue(repository.owner_default)
+ self.assertEqual(team, repository.owner)
+
+ def test_translatePath_create_package_team_member_default(self):
+ # A non-owner member of a team can create a team-owned repository
+ # and immediately set it as that team's default for a package.
+ requester = self.factory.makePerson()
+ team = self.factory.makeTeam(members=[requester])
+ dsp = self.factory.makeDistributionSourcePackage()
+ path = u"/~%s/%s/+source/%s" % (
+ team.name, dsp.distribution.name, dsp.sourcepackagename.name)
+ repository = self.assertCreates(requester, path)
+ self.assertFalse(repository.target_default)
+ self.assertTrue(repository.owner_default)
+ self.assertEqual(team, repository.owner)
def test_translatePath_create_broken_hosting_service(self):
# If the hosting service is down, trying to create a repository
Follow ups