launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #22886
[Merge] lp:~cjwatson/launchpad/git-activity-model into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/git-activity-model into lp:launchpad with lp:~cjwatson/launchpad/git-permissions-model as a prerequisite.
Commit message:
Add GitActivity model, currently logging additions, changes, and removals of rules and grants.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1517559 in Launchpad itself: "git fine-grained permissions"
https://bugs.launchpad.net/launchpad/+bug/1517559
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/git-activity-model/+merge/354399
See https://docs.google.com/document/d/1JW_D_Tgo4X2-vPMZtShSbi3cm1iOsGcNIzeOpa5E_wA for the design and https://code.launchpad.net/~cjwatson/launchpad/db-git-activity/+merge/354398 for schema changes.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/git-activity-model into lp:launchpad.
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg 2018-09-06 14:26:10 +0000
+++ database/schema/security.cfg 2018-09-06 14:26:10 +0000
@@ -187,6 +187,7 @@
public.featureflag = SELECT, INSERT, UPDATE, DELETE
public.featureflagchangelogentry = SELECT, INSERT, UPDATE
public.flatpackagesetinclusion = SELECT, INSERT, UPDATE, DELETE
+public.gitactivity = SELECT, INSERT, UPDATE, DELETE
public.gitgrant = SELECT, INSERT, UPDATE, DELETE
public.gitjob = SELECT, INSERT, UPDATE, DELETE
public.gitref = SELECT, INSERT, UPDATE, DELETE
@@ -2208,6 +2209,7 @@
public.emailaddress = SELECT, UPDATE, DELETE
public.faq = SELECT, UPDATE
public.featureflagchangelogentry = SELECT, UPDATE
+public.gitactivity = SELECT, UPDATE
public.gitgrant = SELECT, UPDATE, DELETE
public.gitrepository = SELECT, UPDATE
public.gitrule = SELECT, UPDATE
=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml 2018-09-06 14:26:10 +0000
+++ lib/lp/code/configure.zcml 2018-09-06 14:26:10 +0000
@@ -948,6 +948,19 @@
for="lp.code.interfaces.gitgrant.IGitGrant zope.lifecycleevent.interfaces.IObjectModifiedEvent"
handler="lp.code.model.gitgrant.git_grant_modified"/>
+ <!-- GitActivity -->
+
+ <class class="lp.code.model.gitactivity.GitActivity">
+ <require
+ permission="launchpad.View"
+ interface="lp.code.interfaces.gitactivity.IGitActivity" />
+ </class>
+ <securedutility
+ class="lp.code.model.gitactivity.GitActivitySet"
+ provides="lp.code.interfaces.gitactivity.IGitActivitySet">
+ <allow interface="lp.code.interfaces.gitactivity.IGitActivitySet" />
+ </securedutility>
+
<!-- GitCollection -->
<class class="lp.code.model.gitcollection.GenericGitCollection">
=== modified file 'lib/lp/code/enums.py'
--- lib/lp/code/enums.py 2018-09-06 14:26:10 +0000
+++ lib/lp/code/enums.py 2018-09-06 14:26:10 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2017 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Enumerations used in the lp/code modules."""
@@ -21,6 +21,7 @@
'CodeImportReviewStatus',
'CodeReviewNotificationLevel',
'CodeReviewVote',
+ 'GitActivityType',
'GitGranteeType',
'GitObjectType',
'GitRepositoryType',
@@ -196,6 +197,25 @@
""")
+class GitActivityType(DBEnumeratedType):
+ """Git Activity Type
+
+ Various kinds of change can be recorded for a Git repository.
+ """
+
+ RULE_ADDED = DBItem(1, "Added access rule")
+
+ RULE_CHANGED = DBItem(2, "Changed access rule")
+
+ RULE_REMOVED = DBItem(3, "Removed access rule")
+
+ GRANT_ADDED = DBItem(4, "Added access grant")
+
+ GRANT_CHANGED = DBItem(5, "Changed access grant")
+
+ GRANT_REMOVED = DBItem(6, "Removed access grant")
+
+
class BranchLifecycleStatusFilter(EnumeratedType):
"""Branch Lifecycle Status Filter
=== added file 'lib/lp/code/interfaces/gitactivity.py'
--- lib/lp/code/interfaces/gitactivity.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/interfaces/gitactivity.py 2018-09-06 14:26:10 +0000
@@ -0,0 +1,121 @@
+# Copyright 2018 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Git repository activity logs."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'IGitActivity',
+ 'IGitActivitySet',
+ ]
+
+from lazr.restful.fields import Reference
+from zope.interface import Interface
+from zope.schema import (
+ Choice,
+ Datetime,
+ Int,
+ TextLine,
+ )
+
+from lp import _
+from lp.code.enums import GitActivityType
+from lp.code.interfaces.gitrepository import IGitRepository
+from lp.services.fields import (
+ PersonChoice,
+ PublicPersonChoice,
+ )
+
+
+class IGitActivity(Interface):
+ """An activity log entry for a Git repository."""
+
+ id = Int(title=_("ID"), readonly=True, required=True)
+
+ repository = Reference(
+ title=_("Repository"), required=True, readonly=True,
+ schema=IGitRepository,
+ description=_("The repository that this log entry is for."))
+
+ date_changed = Datetime(
+ title=_("Date changed"), required=True, readonly=True,
+ description=_("The time when this change happened."))
+
+ changer = PublicPersonChoice(
+ title=_("Changer"), required=True, readonly=True,
+ vocabulary="ValidPerson",
+ description=_("The user who made this change."))
+
+ changee = PersonChoice(
+ title=_("Changee"), required=False, readonly=True,
+ vocabulary="ValidPersonOrTeam",
+ description=_("The person or team that this change was applied to."))
+
+ what_changed = Choice(
+ title=_("What changed"), required=True, readonly=True,
+ vocabulary=GitActivityType,
+ description=_("The property of the repository that changed."))
+
+ old_value = TextLine(
+ title=_("Old value"), required=True, readonly=True,
+ description=_("The value before the change."))
+
+ new_value = TextLine(
+ title=_("New value"), required=True, readonly=True,
+ description=_("The value after the change."))
+
+
+class IGitActivitySet(Interface):
+ """Utilities for managing Git repository activity log entries."""
+
+ def logRuleAdded(grant, user):
+ """Log that an access rule was added.
+
+ :param rule: The `IGitRule` that was added.
+ :param user: The `IPerson` who added it.
+ :return: The new `IGitActivity`.
+ """
+
+ def logRuleChanged(old_grant, new_grant, user):
+ """Log that an access rule was changed.
+
+ :param old_rule: The `IGitRule` before the change.
+ :param new_rule: The `IGitRule` after the change.
+ :param user: The `IPerson` who made the change.
+ :return: The new `IGitActivity`.
+ """
+
+ def logRuleRemoved(grant, user):
+ """Log that an access rule was removed.
+
+ :param rule: The `IGitRule` that was removed.
+ :param user: The `IPerson` who removed it.
+ :return: The new `IGitActivity`.
+ """
+
+ def logGrantAdded(grant, user):
+ """Log that an access grant was added.
+
+ :param grant: The `IGitGrant` that was added.
+ :param user: The `IPerson` who added it.
+ :return: The new `IGitActivity`.
+ """
+
+ def logGrantChanged(old_grant, new_grant, user):
+ """Log that an access grant was changed.
+
+ :param old_grant: The `IGitGrant` before the change.
+ :param new_grant: The `IGitGrant` after the change.
+ :param user: The `IPerson` who made the change.
+ :return: The new `IGitActivity`.
+ """
+
+ def logGrantRemoved(grant, user):
+ """Log that an access grant was removed.
+
+ :param grant: The `IGitGrant` that was removed.
+ :param user: The `IPerson` who removed it.
+ :return: The new `IGitActivity`.
+ """
=== modified file 'lib/lp/code/interfaces/gitgrant.py'
--- lib/lp/code/interfaces/gitgrant.py 2018-09-06 14:26:10 +0000
+++ lib/lp/code/interfaces/gitgrant.py 2018-09-06 14:26:10 +0000
@@ -90,8 +90,11 @@
class IGitGrantEdit(Interface):
"""`IGitGrant` attributes that require launchpad.Edit."""
- def destroySelf():
- """Delete this access grant."""
+ def destroySelf(user):
+ """Delete this access grant.
+
+ :param user: The `IPerson` doing the deletion.
+ """
class IGitGrant(IGitGrantView, IGitGrantEditableAttributes, IGitGrantEdit):
=== modified file 'lib/lp/code/interfaces/gitrepository.py'
--- lib/lp/code/interfaces/gitrepository.py 2018-09-06 14:26:10 +0000
+++ lib/lp/code/interfaces/gitrepository.py 2018-09-06 14:26:10 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015-2017 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2018 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Git repository interfaces."""
@@ -270,6 +270,8 @@
grants = Attribute("The access grants for this repository.")
+ activity = Attribute("The activity log entries for this repository.")
+
@operation_parameters(
path=TextLine(title=_("A string to look up as a path.")))
# Really IGitRef, patched in _schema_circular_imports.py.
=== modified file 'lib/lp/code/interfaces/gitrule.py'
--- lib/lp/code/interfaces/gitrule.py 2018-09-06 14:26:10 +0000
+++ lib/lp/code/interfaces/gitrule.py 2018-09-06 14:26:10 +0000
@@ -82,8 +82,11 @@
matching this rule.
"""
- def destroySelf():
- """Delete this rule."""
+ def destroySelf(user):
+ """Delete this rule.
+
+ :param user: The `IPerson` doing the deletion.
+ """
class IGitRule(IGitRuleView, IGitRuleEditableAttributes, IGitRuleEdit):
=== added file 'lib/lp/code/model/gitactivity.py'
--- lib/lp/code/model/gitactivity.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/model/gitactivity.py 2018-09-06 14:26:10 +0000
@@ -0,0 +1,128 @@
+# Copyright 2018 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Git repository activity logs."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'GitActivity',
+ ]
+
+import pytz
+from storm.locals import (
+ DateTime,
+ Int,
+ JSON,
+ Reference,
+ )
+from zope.interface import implementer
+
+from lp.code.enums import GitActivityType
+from lp.code.interfaces.gitactivity import (
+ IGitActivity,
+ IGitActivitySet,
+ )
+from lp.registry.interfaces.person import (
+ validate_person,
+ validate_public_person,
+ )
+from lp.services.database.constants import DEFAULT
+from lp.services.database.enumcol import EnumCol
+from lp.services.database.stormbase import StormBase
+
+
+@implementer(IGitActivity)
+class GitActivity(StormBase):
+ """See IGitActivity`."""
+
+ __storm_table__ = 'GitActivity'
+
+ id = Int(primary=True)
+
+ repository_id = Int(name='repository', allow_none=False)
+ repository = Reference(repository_id, 'GitRepository.id')
+
+ date_changed = DateTime(
+ name='date_changed', tzinfo=pytz.UTC, allow_none=False)
+
+ changer_id = Int(
+ name='changer', allow_none=False, validator=validate_public_person)
+ changer = Reference(changer_id, 'Person.id')
+
+ changee_id = Int(
+ name='changee', allow_none=True, validator=validate_person)
+ changee = Reference(changee_id, 'Person.id')
+
+ what_changed = EnumCol(
+ dbName='what_changed', enum=GitActivityType, notNull=True)
+
+ old_value = JSON(name='old_value', allow_none=True)
+ new_value = JSON(name='new_value', allow_none=True)
+
+ def __init__(self, repository, changer, what_changed, changee=None,
+ old_value=None, new_value=None, date_changed=DEFAULT):
+ super(GitActivity, self).__init__()
+ self.repository = repository
+ self.date_changed = date_changed
+ self.changer = changer
+ self.changee = changee
+ self.what_changed = what_changed
+ self.old_value = old_value
+ self.new_value = new_value
+
+
+def _make_rule_value(rule):
+ return {"ref_pattern": rule.ref_pattern}
+
+
+def _make_grant_value(grant):
+ return {
+ # If the grantee is a person, then we store that in
+ # GitActivity.changee; this makes it visible to person merges and so
+ # on.
+ "grantee_type": grant.grantee_type.title,
+ "can_create": grant.can_create,
+ "can_push": grant.can_push,
+ "can_force_push": grant.can_force_push,
+ }
+
+
+@implementer(IGitActivitySet)
+class GitActivitySet:
+
+ def logRuleAdded(self, rule, user):
+ return GitActivity(
+ rule.repository, user, GitActivityType.RULE_ADDED,
+ new_value=_make_rule_value(rule))
+
+ def logRuleChanged(self, old_rule, new_rule, user):
+ return GitActivity(
+ new_rule.repository, user, GitActivityType.RULE_CHANGED,
+ old_value=_make_rule_value(old_rule),
+ new_value=_make_rule_value(new_rule))
+
+ def logRuleRemoved(self, rule, user):
+ return GitActivity(
+ rule.repository, user, GitActivityType.RULE_REMOVED,
+ old_value=_make_rule_value(rule))
+
+ def logGrantAdded(self, grant, user):
+ return GitActivity(
+ grant.repository, user, GitActivityType.GRANT_ADDED,
+ changee=grant.grantee,
+ new_value=_make_grant_value(grant))
+
+ def logGrantChanged(self, old_grant, new_grant, user):
+ return GitActivity(
+ new_grant.repository, user, GitActivityType.GRANT_CHANGED,
+ changee=new_grant.grantee,
+ old_value=_make_grant_value(old_grant),
+ new_value=_make_grant_value(new_grant))
+
+ def logGrantRemoved(self, grant, user):
+ return GitActivity(
+ grant.repository, user, GitActivityType.GRANT_REMOVED,
+ changee=grant.grantee,
+ old_value=_make_grant_value(grant))
=== modified file 'lib/lp/code/model/gitgrant.py'
--- lib/lp/code/model/gitgrant.py 2018-09-06 14:26:10 +0000
+++ lib/lp/code/model/gitgrant.py 2018-09-06 14:26:10 +0000
@@ -19,11 +19,15 @@
Reference,
Store,
)
+from zope.component import getUtility
from zope.interface import implementer
+from zope.security.proxy import removeSecurityProxy
from lp.code.enums import GitGranteeType
+from lp.code.interfaces.gitactivity import IGitActivitySet
from lp.code.interfaces.gitgrant import IGitGrant
from lp.registry.interfaces.person import (
+ IPerson,
validate_person,
validate_public_person,
)
@@ -39,7 +43,10 @@
events on Git repository grants.
"""
if event.edited_fields:
- grant.date_last_modified = UTC_NOW
+ user = IPerson(event.user)
+ getUtility(IGitActivitySet).logGrantChanged(
+ event.object_before_modification, grant, user)
+ removeSecurityProxy(grant).date_last_modified = UTC_NOW
@implementer(IGitGrant)
@@ -114,6 +121,7 @@
return "<GitGrant [%s] to %s> for %r" % (
", ".join(permissions), grantee_name, self.rule)
- def destroySelf(self):
+ def destroySelf(self, user):
"""See `IGitGrant`."""
+ getUtility(IGitActivitySet).logGrantRemoved(self, user)
Store.of(self).remove(self)
=== modified file 'lib/lp/code/model/gitrepository.py'
--- lib/lp/code/model/gitrepository.py 2018-09-06 14:26:10 +0000
+++ lib/lp/code/model/gitrepository.py 2018-09-06 14:26:10 +0000
@@ -89,6 +89,7 @@
notify_modified,
)
from lp.code.interfaces.codeimport import ICodeImportSet
+from lp.code.interfaces.gitactivity import IGitActivitySet
from lp.code.interfaces.gitcollection import (
IAllGitRepositories,
IGitCollection,
@@ -109,6 +110,7 @@
from lp.code.interfaces.revision import IRevisionSet
from lp.code.mail.branch import send_git_repository_modified_notifications
from lp.code.model.branchmergeproposal import BranchMergeProposal
+from lp.code.model.gitactivity import GitActivity
from lp.code.model.gitgrant import GitGrant
from lp.code.model.gitref import (
GitRef,
@@ -1152,6 +1154,7 @@
self.rule_order.append(rule.id)
else:
self.rule_order.insert(position, rule.id)
+ getUtility(IGitActivitySet).logRuleAdded(rule, creator)
return rule
@property
@@ -1159,6 +1162,14 @@
"""See `IGitRepository`."""
return Store.of(self).find(GitGrant, GitGrant.repository_id == self.id)
+ @property
+ def activity(self):
+ """See `IGitRepository`."""
+ rows = Store.of(self).find(
+ GitActivity, GitActivity.repository_id == self.id)
+ return rows.order_by(
+ Desc(GitActivity.date_changed), Desc(GitActivity.id))
+
def canBeDeleted(self):
"""See `IGitRepository`."""
# Can't delete if the repository is associated with anything.
=== modified file 'lib/lp/code/model/gitrule.py'
--- lib/lp/code/model/gitrule.py 2018-09-06 14:26:10 +0000
+++ lib/lp/code/model/gitrule.py 2018-09-06 14:26:10 +0000
@@ -18,11 +18,17 @@
Store,
Unicode,
)
+from zope.component import getUtility
from zope.interface import implementer
+from zope.security.proxy import removeSecurityProxy
+from lp.code.interfaces.gitactivity import IGitActivitySet
from lp.code.interfaces.gitrule import IGitRule
from lp.code.model.gitgrant import GitGrant
-from lp.registry.interfaces.person import validate_public_person
+from lp.registry.interfaces.person import (
+ IPerson,
+ validate_public_person,
+ )
from lp.services.database.constants import (
DEFAULT,
UTC_NOW,
@@ -37,7 +43,10 @@
events on Git repository rules.
"""
if event.edited_fields:
- rule.date_last_modified = UTC_NOW
+ user = IPerson(event.user)
+ getUtility(IGitActivitySet).logRuleChanged(
+ event.object_before_modification, rule, user)
+ removeSecurityProxy(rule).date_last_modified = UTC_NOW
@implementer(IGitRule)
@@ -81,12 +90,15 @@
def addGrant(self, grantee, grantor, can_create=False, can_push=False,
can_force_push=False):
"""See `IGitRule`."""
- return GitGrant(
+ grant = GitGrant(
rule=self, grantee=grantee, can_create=can_create,
can_push=can_push, can_force_push=can_force_push, grantor=grantor,
date_created=DEFAULT)
+ getUtility(IGitActivitySet).logGrantAdded(grant, grantor)
+ return grant
- def destroySelf(self):
+ def destroySelf(self, user):
"""See `IGitRule`."""
+ getUtility(IGitActivitySet).logRuleRemoved(self, user)
self.repository.rule_order.remove(self.id)
Store.of(self).remove(self)
=== added file 'lib/lp/code/model/tests/test_gitactivity.py'
--- lib/lp/code/model/tests/test_gitactivity.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/model/tests/test_gitactivity.py 2018-09-06 14:26:10 +0000
@@ -0,0 +1,48 @@
+# Copyright 2018 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for Git repository activity logs."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+
+from storm.store import Store
+from testtools.matchers import MatchesStructure
+
+from lp.code.enums import GitActivityType
+from lp.code.interfaces.gitactivity import IGitActivity
+from lp.code.model.gitactivity import GitActivity
+from lp.services.database.sqlbase import get_transaction_timestamp
+from lp.testing import (
+ TestCaseWithFactory,
+ verifyObject,
+ )
+from lp.testing.layers import DatabaseFunctionalLayer
+
+
+class TestGitActivity(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def test_implements_IGitActivity(self):
+ repository = self.factory.makeGitRepository()
+ activity = GitActivity(
+ repository, repository.owner, GitActivityType.RULE_ADDED)
+ verifyObject(IGitActivity, activity)
+
+ def test_properties(self):
+ repository = self.factory.makeGitRepository()
+ changee = self.factory.makePerson()
+ activity = GitActivity(
+ repository, repository.owner, GitActivityType.RULE_ADDED,
+ changee=changee, old_value={"old": None}, new_value={"new": None})
+ now = get_transaction_timestamp(Store.of(activity))
+ self.assertThat(activity, MatchesStructure.byEquality(
+ repository=repository,
+ date_changed=now,
+ changer=repository.owner,
+ changee=changee,
+ what_changed=GitActivityType.RULE_ADDED,
+ old_value={"old": None},
+ new_value={"new": None}))
=== modified file 'lib/lp/code/model/tests/test_gitgrant.py'
--- lib/lp/code/model/tests/test_gitgrant.py 2018-09-06 14:26:10 +0000
+++ lib/lp/code/model/tests/test_gitgrant.py 2018-09-06 14:26:10 +0000
@@ -7,15 +7,23 @@
__metaclass__ = type
+from lazr.lifecycle.event import ObjectModifiedEvent
+from lazr.lifecycle.snapshot import Snapshot
from storm.store import Store
from testtools.matchers import (
Equals,
Is,
+ MatchesDict,
MatchesSetwise,
MatchesStructure,
)
+from zope.event import notify
+from zope.interface import providedBy
-from lp.code.enums import GitGranteeType
+from lp.code.enums import (
+ GitActivityType,
+ GitGranteeType,
+ )
from lp.code.interfaces.gitgrant import IGitGrant
from lp.services.database.sqlbase import get_transaction_timestamp
from lp.testing import (
@@ -92,6 +100,80 @@
"<GitGrant [push] to ~%s> for %r" % (grantee.name, rule),
repr(grant))
+ def test_activity_grant_added(self):
+ owner = self.factory.makeTeam()
+ member = self.factory.makePerson(member_of=[owner])
+ repository = self.factory.makeGitRepository(owner=owner)
+ grant = self.factory.makeGitGrant(
+ repository=repository, grantor=member, can_push=True)
+ self.assertThat(repository.activity.first(), MatchesStructure(
+ repository=Equals(repository),
+ changer=Equals(member),
+ changee=Equals(grant.grantee),
+ what_changed=Equals(GitActivityType.GRANT_ADDED),
+ old_value=Is(None),
+ new_value=MatchesDict({
+ "grantee_type": Equals("Person"),
+ "can_create": Is(False),
+ "can_push": Is(True),
+ "can_force_push": Is(False),
+ })))
+
+ def test_activity_grant_changed(self):
+ owner = self.factory.makeTeam()
+ member = self.factory.makePerson(member_of=[owner])
+ repository = self.factory.makeGitRepository(owner=owner)
+ grant = self.factory.makeGitGrant(
+ repository=repository, grantee=GitGranteeType.REPOSITORY_OWNER,
+ can_create=True)
+ grant_before_modification = Snapshot(
+ grant, providing=providedBy(grant))
+ with person_logged_in(member):
+ grant.can_create = False
+ grant.can_force_push = True
+ notify(ObjectModifiedEvent(
+ grant, grant_before_modification,
+ ["can_create", "can_force_push"]))
+ self.assertThat(repository.activity.first(), MatchesStructure(
+ repository=Equals(repository),
+ changer=Equals(member),
+ changee=Is(None),
+ what_changed=Equals(GitActivityType.GRANT_CHANGED),
+ old_value=MatchesDict({
+ "grantee_type": Equals("Repository owner"),
+ "can_create": Is(True),
+ "can_push": Is(False),
+ "can_force_push": Is(False),
+ }),
+ new_value=MatchesDict({
+ "grantee_type": Equals("Repository owner"),
+ "can_create": Is(False),
+ "can_push": Is(False),
+ "can_force_push": Is(True),
+ })))
+
+ def test_activity_grant_removed(self):
+ owner = self.factory.makeTeam()
+ member = self.factory.makePerson(member_of=[owner])
+ repository = self.factory.makeGitRepository(owner=owner)
+ grant = self.factory.makeGitGrant(
+ repository=repository, can_create=True, can_push=True)
+ grantee = grant.grantee
+ with person_logged_in(member):
+ grant.destroySelf(member)
+ self.assertThat(repository.activity.first(), MatchesStructure(
+ repository=Equals(repository),
+ changer=Equals(member),
+ changee=Equals(grantee),
+ what_changed=Equals(GitActivityType.GRANT_REMOVED),
+ old_value=MatchesDict({
+ "grantee_type": Equals("Person"),
+ "can_create": Is(True),
+ "can_push": Is(True),
+ "can_force_push": Is(False),
+ }),
+ new_value=Is(None)))
+
def test_destroySelf(self):
rule = self.factory.makeGitRule()
grants = [
@@ -101,5 +183,5 @@
self.factory.makeGitGrant(rule=rule, can_push=True),
]
with person_logged_in(rule.repository.owner):
- grants[1].destroySelf()
+ grants[1].destroySelf(rule.repository.owner)
self.assertThat(rule.grants, MatchesSetwise(Equals(grants[0])))
=== modified file 'lib/lp/code/model/tests/test_gitrule.py'
--- lib/lp/code/model/tests/test_gitrule.py 2018-09-06 14:26:10 +0000
+++ lib/lp/code/model/tests/test_gitrule.py 2018-09-06 14:26:10 +0000
@@ -7,18 +7,27 @@
__metaclass__ = type
+from lazr.lifecycle.event import ObjectModifiedEvent
+from lazr.lifecycle.snapshot import Snapshot
from storm.store import Store
from testtools.matchers import (
Equals,
Is,
+ MatchesDict,
MatchesSetwise,
MatchesStructure,
)
+from zope.event import notify
+from zope.interface import providedBy
-from lp.code.enums import GitGranteeType
+from lp.code.enums import (
+ GitActivityType,
+ GitGranteeType,
+ )
from lp.code.interfaces.gitrule import IGitRule
from lp.services.database.sqlbase import get_transaction_timestamp
from lp.testing import (
+ person_logged_in,
TestCaseWithFactory,
verifyObject,
)
@@ -90,3 +99,55 @@
can_create=Is(False),
can_push=Is(False),
can_force_push=Is(True))))
+
+ def test_activity_rule_added(self):
+ owner = self.factory.makeTeam()
+ member = self.factory.makePerson(member_of=[owner])
+ repository = self.factory.makeGitRepository(owner=owner)
+ self.factory.makeGitRule(
+ repository=repository, ref_pattern="refs/heads/stable/*",
+ creator=member)
+ self.assertThat(repository.activity.one(), MatchesStructure(
+ repository=Equals(repository),
+ changer=Equals(member),
+ changee=Is(None),
+ what_changed=Equals(GitActivityType.RULE_ADDED),
+ old_value=Is(None),
+ new_value=MatchesDict(
+ {"ref_pattern": Equals("refs/heads/stable/*")})))
+
+ def test_activity_rule_changed(self):
+ owner = self.factory.makeTeam()
+ member = self.factory.makePerson(member_of=[owner])
+ repository = self.factory.makeGitRepository(owner=owner)
+ rule = self.factory.makeGitRule(
+ repository=repository, ref_pattern="refs/heads/*")
+ rule_before_modification = Snapshot(rule, providing=providedBy(rule))
+ with person_logged_in(member):
+ rule.ref_pattern = "refs/heads/other/*"
+ notify(ObjectModifiedEvent(
+ rule, rule_before_modification, ["ref_pattern"]))
+ self.assertThat(repository.activity.first(), MatchesStructure(
+ repository=Equals(repository),
+ changer=Equals(member),
+ changee=Is(None),
+ what_changed=Equals(GitActivityType.RULE_CHANGED),
+ old_value=MatchesDict({"ref_pattern": Equals("refs/heads/*")}),
+ new_value=MatchesDict(
+ {"ref_pattern": Equals("refs/heads/other/*")})))
+
+ def test_activity_rule_removed(self):
+ owner = self.factory.makeTeam()
+ member = self.factory.makePerson(member_of=[owner])
+ repository = self.factory.makeGitRepository(owner=owner)
+ rule = self.factory.makeGitRule(
+ repository=repository, ref_pattern="refs/heads/*")
+ with person_logged_in(member):
+ rule.destroySelf(member)
+ self.assertThat(repository.activity.first(), MatchesStructure(
+ repository=Equals(repository),
+ changer=Equals(member),
+ changee=Is(None),
+ what_changed=Equals(GitActivityType.RULE_REMOVED),
+ old_value=MatchesDict({"ref_pattern": Equals("refs/heads/*")}),
+ new_value=Is(None)))
=== modified file 'lib/lp/security.py'
--- lib/lp/security.py 2018-09-06 14:26:10 +0000
+++ lib/lp/security.py 2018-09-06 14:26:10 +0000
@@ -81,6 +81,7 @@
)
from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference
from lp.code.interfaces.diff import IPreviewDiff
+from lp.code.interfaces.gitactivity import IGitActivity
from lp.code.interfaces.gitcollection import IGitCollection
from lp.code.interfaces.gitgrant import IGitGrant
from lp.code.interfaces.gitref import IGitRef
@@ -2381,6 +2382,15 @@
super(EditGitGrant, self).__init__(obj, obj.repository)
+class ViewGitActivity(DelegatedAuthorization):
+ """Anyone who can see a Git repository can see its activity logs."""
+ permission = 'launchpad.View'
+ usedfor = IGitActivity
+
+ def __init__(self, obj):
+ super(ViewGitActivity, self).__init__(obj, obj.repository)
+
+
class AdminDistroSeriesTranslations(AuthorizationBase):
permission = 'launchpad.TranslationsAdmin'
usedfor = IDistroSeries
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2018-09-06 14:26:10 +0000
+++ lib/lp/testing/factory.py 2018-09-06 14:26:10 +0000
@@ -1839,10 +1839,11 @@
return repository.addRule(ref_pattern, creator, position=position)
def makeGitGrant(self, rule=None, grantee=None, grantor=None,
- can_create=False, can_push=False, can_force_push=False):
+ can_create=False, can_push=False, can_force_push=False,
+ **rule_kwargs):
"""Create a Git repository access grant."""
if rule is None:
- rule = self.makeGitRule()
+ rule = self.makeGitRule(**rule_kwargs)
if grantee is None:
grantee = self.makePerson()
if grantor is None:
Follow ups