Merge ~pappacena/launchpad:git-repo-async-privacy into launchpad:master

Proposed by Thiago F. Pappacena
Status: Needs review
Proposed branch: ~pappacena/launchpad:git-repo-async-privacy
Merge into: launchpad:master
Diff against target: 734 lines (+312/-31)
17 files modified
database/schema/security.cfg (+18/-0)
lib/lp/app/widgets/itemswidgets.py (+6/-2)
lib/lp/code/browser/gitrepository.py (+9/-1)
lib/lp/code/browser/tests/test_branchmergeproposal.py (+1/-1)
lib/lp/code/browser/tests/test_gitrepository.py (+69/-3)
lib/lp/code/configure.zcml (+9/-0)
lib/lp/code/interfaces/gitjob.py (+20/-1)
lib/lp/code/interfaces/gitrepository.py (+4/-0)
lib/lp/code/model/gitjob.py (+57/-0)
lib/lp/code/model/gitref.py (+1/-1)
lib/lp/code/model/gitrepository.py (+42/-9)
lib/lp/code/model/tests/test_gitcollection.py (+1/-1)
lib/lp/code/model/tests/test_gitjob.py (+59/-0)
lib/lp/code/model/tests/test_gitrepository.py (+4/-6)
lib/lp/code/xmlrpc/tests/test_git.py (+5/-5)
lib/lp/services/config/schema-lazr.conf (+6/-0)
lib/lp/testing/factory.py (+1/-1)
Reviewer Review Type Date Requested Status
Colin Watson (community) Needs Information
Ioana Lasc (community) Approve
Review via email: mp+392776@code.launchpad.net

Commit message

Doing repository privacy change in background, and blocking user from changing it while another change is in progress.

To post a comment you must log in.
Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

This MP is already quite big, so I'll create in another MP the garbo job that will deal with GitRepositories that are PENDING_INFORMATION_TYPE_TRANSITION for too long (to avoid blocking them forever).

Revision history for this message
Ioana Lasc (ilasc) wrote :

LGTM

review: Approve
974b22a... by Thiago F. Pappacena

Fixing test

Revision history for this message
Colin Watson (cjwatson) wrote :

I think this is mostly OK, but I wonder if it could be even better. Instead of adding a new item to GitRepositoryStatus, have you considered just checking whether there's a pending job of the appropriate type, a bit like GitRepository.pending_updates? That would have the advantage that there'd be no need for a garbo job (https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/392809) to clean things up if the information-type-changing job fails: it would just automatically become available again, which feels like a cleaner design.

review: Needs Information
826cbad... by Thiago F. Pappacena

Encapsulating GitRepository.pending_change_information_type

eeeb231... by Thiago F. Pappacena

Renaming GitRepo information type change job

2493c1d... by Thiago F. Pappacena

Changing exception msg when blocking info type transition for git repo

Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

Pushed the requested changes.

I thought about just checking on-the-fly for unfinished jobs, but didn't go that way because it could be slightly expensive to run such query. But we do that in so few places that I agree it doesn't worth to concentrate that check in a garbo job. I have changed it too.

Unmerged commits

2493c1d... by Thiago F. Pappacena

Changing exception msg when blocking info type transition for git repo

eeeb231... by Thiago F. Pappacena

Renaming GitRepo information type change job

826cbad... by Thiago F. Pappacena

Encapsulating GitRepository.pending_change_information_type

974b22a... by Thiago F. Pappacena

Fixing test

4bd7dcc... by Thiago F. Pappacena

Fixing configuration of info type transition job

76e0272... by Thiago F. Pappacena

Blocking at the UI information type change while it is already changing

caf2673... by Thiago F. Pappacena

Adding tests for async GitRepo.transitionToInformationType

3a5e053... by Thiago F. Pappacena

Changing GitRepo.transitionInformationType method to be async

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/database/schema/security.cfg b/database/schema/security.cfg
index 11aba6f..290e7bc 100644
--- a/database/schema/security.cfg
+++ b/database/schema/security.cfg
@@ -2082,6 +2082,24 @@ public.webhookjob = SELECT, INSERT
2082public.xref = SELECT, INSERT, DELETE2082public.xref = SELECT, INSERT, DELETE
2083type=user2083type=user
20842084
2085[privacy-change-jobs]
2086groups=script
2087public.accessartifact = SELECT, UPDATE, DELETE, INSERT
2088public.accessartifactgrant = SELECT, UPDATE, DELETE, INSERT
2089public.accesspolicyartifact = SELECT, UPDATE, DELETE, INSERT
2090public.accesspolicygrant = SELECT, UPDATE, DELETE
2091public.account = SELECT
2092public.distribution = SELECT
2093public.gitjob = SELECT, UPDATE
2094public.gitrepository = SELECT, UPDATE
2095public.gitsubscription = SELECT, UPDATE, DELETE
2096public.job = SELECT, INSERT, UPDATE
2097public.person = SELECT
2098public.product = SELECT
2099public.sharingjob = SELECT, INSERT, UPDATE
2100public.teamparticipation = SELECT
2101type=user
2102
2085[sharing-jobs]2103[sharing-jobs]
2086groups=script2104groups=script
2087public.accessartifactgrant = SELECT, UPDATE, DELETE2105public.accessartifactgrant = SELECT, UPDATE, DELETE
diff --git a/lib/lp/app/widgets/itemswidgets.py b/lib/lp/app/widgets/itemswidgets.py
index 1dbb59f..a644a96 100644
--- a/lib/lp/app/widgets/itemswidgets.py
+++ b/lib/lp/app/widgets/itemswidgets.py
@@ -189,25 +189,29 @@ class LaunchpadRadioWidgetWithDescription(LaunchpadRadioWidget):
189 """Render an item of the list."""189 """Render an item of the list."""
190 text = html_escape(text)190 text = html_escape(text)
191 id = '%s.%s' % (name, index)191 id = '%s.%s' % (name, index)
192 extra_attr = {"disabled": "disabled"} if self.context.readonly else {}
192 elem = renderElement(u'input',193 elem = renderElement(u'input',
193 value=value,194 value=value,
194 name=name,195 name=name,
195 id=id,196 id=id,
196 cssClass=cssClass,197 cssClass=cssClass,
197 type='radio')198 type='radio',
199 **extra_attr)
198 return self._renderRow(text, value, id, elem)200 return self._renderRow(text, value, id, elem)
199201
200 def renderSelectedItem(self, index, text, value, name, cssClass):202 def renderSelectedItem(self, index, text, value, name, cssClass):
201 """Render a selected item of the list."""203 """Render a selected item of the list."""
202 text = html_escape(text)204 text = html_escape(text)
203 id = '%s.%s' % (name, index)205 id = '%s.%s' % (name, index)
206 extra_attr = {"disabled": "disabled"} if self.context.readonly else {}
204 elem = renderElement(u'input',207 elem = renderElement(u'input',
205 value=value,208 value=value,
206 name=name,209 name=name,
207 id=id,210 id=id,
208 cssClass=cssClass,211 cssClass=cssClass,
209 checked="checked",212 checked="checked",
210 type='radio')213 type='radio',
214 **extra_attr)
211 return self._renderRow(text, value, id, elem)215 return self._renderRow(text, value, id, elem)
212216
213 def renderExtraHint(self):217 def renderExtraHint(self):
diff --git a/lib/lp/code/browser/gitrepository.py b/lib/lp/code/browser/gitrepository.py
index 2552ffb..1ec1b16 100644
--- a/lib/lp/code/browser/gitrepository.py
+++ b/lib/lp/code/browser/gitrepository.py
@@ -483,6 +483,8 @@ class GitRepositoryView(InformationTypePortletMixin, LaunchpadView,
483 def warning_message(self):483 def warning_message(self):
484 if self.context.status == GitRepositoryStatus.CREATING:484 if self.context.status == GitRepositoryStatus.CREATING:
485 return "This repository is being created."485 return "This repository is being created."
486 if self.context.pending_change_information_type:
487 return "This repository's information type is being changed."
486 return None488 return None
487489
488 @property490 @property
@@ -573,6 +575,7 @@ class GitRepositoryEditFormView(LaunchpadEditFormView):
573 @cachedproperty575 @cachedproperty
574 def schema(self):576 def schema(self):
575 info_types = self.getInformationTypesToShow()577 info_types = self.getInformationTypesToShow()
578 read_only_info_type = self.context.pending_change_information_type
576579
577 class GitRepositoryEditSchema(Interface):580 class GitRepositoryEditSchema(Interface):
578 """Defines the fields for the edit form.581 """Defines the fields for the edit form.
@@ -582,7 +585,8 @@ class GitRepositoryEditFormView(LaunchpadEditFormView):
582 """585 """
583 use_template(IGitRepository, include=["default_branch"])586 use_template(IGitRepository, include=["default_branch"])
584 information_type = copy_field(587 information_type = copy_field(
585 IGitRepository["information_type"], readonly=False,588 IGitRepository["information_type"],
589 readonly=read_only_info_type,
586 vocabulary=InformationTypeVocabulary(types=info_types))590 vocabulary=InformationTypeVocabulary(types=info_types))
587 name = copy_field(IGitRepository["name"], readonly=False)591 name = copy_field(IGitRepository["name"], readonly=False)
588 owner = copy_field(IGitRepository["owner"], readonly=False)592 owner = copy_field(IGitRepository["owner"], readonly=False)
@@ -785,6 +789,10 @@ class GitRepositoryEditView(CodeEditOwnerMixin, GitRepositoryEditFormView):
785 self.widgets["target"].hint = (789 self.widgets["target"].hint = (
786 "This is the default repository for this target, so it "790 "This is the default repository for this target, so it "
787 "cannot be moved to another target.")791 "cannot be moved to another target.")
792 if self.context.pending_change_information_type:
793 self.widgets["information_type"].hint = (
794 "The information type is being changed. The operation needs "
795 "to finish before you can change it again.")
788 if self.context.default_branch:796 if self.context.default_branch:
789 self.widgets['default_branch'].context.required = True797 self.widgets['default_branch'].context.required = True
790798
diff --git a/lib/lp/code/browser/tests/test_branchmergeproposal.py b/lib/lp/code/browser/tests/test_branchmergeproposal.py
index f2ec4ec..83c51dc 100644
--- a/lib/lp/code/browser/tests/test_branchmergeproposal.py
+++ b/lib/lp/code/browser/tests/test_branchmergeproposal.py
@@ -2336,7 +2336,7 @@ class TestLatestProposalsForEachBranchGit(
23362336
2337 @staticmethod2337 @staticmethod
2338 def _setBranchInvisible(branch):2338 def _setBranchInvisible(branch):
2339 removeSecurityProxy(branch.repository).transitionToInformationType(2339 removeSecurityProxy(branch.repository)._transitionToInformationType(
2340 InformationType.USERDATA, branch.owner, verify_policy=False)2340 InformationType.USERDATA, branch.owner, verify_policy=False)
23412341
23422342
diff --git a/lib/lp/code/browser/tests/test_gitrepository.py b/lib/lp/code/browser/tests/test_gitrepository.py
index 262d038..13a3a16 100644
--- a/lib/lp/code/browser/tests/test_gitrepository.py
+++ b/lib/lp/code/browser/tests/test_gitrepository.py
@@ -60,6 +60,9 @@ from lp.code.enums import (
60 GitRepositoryType,60 GitRepositoryType,
61 )61 )
62from lp.code.interfaces.gitcollection import IGitCollection62from lp.code.interfaces.gitcollection import IGitCollection
63from lp.code.interfaces.gitjob import (
64 IGitRepositoryChangeInformationTypeJobSource,
65 )
63from lp.code.interfaces.gitrepository import IGitRepositorySet66from lp.code.interfaces.gitrepository import IGitRepositorySet
64from lp.code.interfaces.revision import IRevisionSet67from lp.code.interfaces.revision import IRevisionSet
65from lp.code.model.gitjob import GitRefScanJob68from lp.code.model.gitjob import GitRefScanJob
@@ -161,6 +164,15 @@ class TestGitRepositoryView(BrowserTestCase):
161 self.assertTextMatchesExpressionIgnoreWhitespace(164 self.assertTextMatchesExpressionIgnoreWhitespace(
162 r"""This repository is being created\..*""", text)165 r"""This repository is being created\..*""", text)
163166
167 def test_changing_info_type_warning_message_is_present(self):
168 repository = removeSecurityProxy(self.factory.makeGitRepository())
169 repository.transitionToInformationType(
170 InformationType.PRIVATESECURITY, repository.owner)
171 text = self.getMainText(repository, "+index", user=repository.owner)
172 self.assertTextMatchesExpressionIgnoreWhitespace(
173 r"""This repository's information type is being changed\..*""",
174 text)
175
164 def test_creating_warning_message_is_not_shown(self):176 def test_creating_warning_message_is_not_shown(self):
165 repository = removeSecurityProxy(self.factory.makeGitRepository())177 repository = removeSecurityProxy(self.factory.makeGitRepository())
166 repository.status = GitRepositoryStatus.AVAILABLE178 repository.status = GitRepositoryStatus.AVAILABLE
@@ -1185,8 +1197,54 @@ class TestGitRepositoryEditView(TestCaseWithFactory):
1185 browser.getControl("Private", index=1).click()1197 browser.getControl("Private", index=1).click()
1186 browser.getControl("Change Git Repository").click()1198 browser.getControl("Change Git Repository").click()
1187 with person_logged_in(person):1199 with person_logged_in(person):
1188 self.assertEqual(1200 self.assertTrue(repository.pending_change_information_type)
1189 InformationType.USERDATA, repository.information_type)1201 job_util = getUtility(
1202 IGitRepositoryChangeInformationTypeJobSource)
1203 jobs = list(job_util.iterReady())
1204 self.assertEqual(1, len(jobs))
1205 job = removeSecurityProxy(jobs[0])
1206 self.assertEqual(repository, job.repository)
1207 self.assertEqual(InformationType.USERDATA, job.information_type)
1208 self.assertEqual(admin, job.user)
1209
1210 def test_information_type_in_ui_blocked_if_already_changing(self):
1211 # The information_type of a repository can't be changed via the UI
1212 # if the repository is already pending a info type change.
1213 person = self.factory.makePerson()
1214 repository = self.factory.makeGitRepository(owner=person)
1215 with admin_logged_in():
1216 repository.transitionToInformationType(
1217 InformationType.PRIVATESECURITY, repository.owner)
1218 # There should be 1 job pending to be executed.
1219 job_util = getUtility(
1220 IGitRepositoryChangeInformationTypeJobSource)
1221 jobs = list(job_util.iterReady())
1222 self.assertEqual(1, len(jobs))
1223 admin = getUtility(ILaunchpadCelebrities).admin.teamowner
1224 browser = self.getUserBrowser(
1225 canonical_url(repository) + "/+edit", user=admin)
1226 # Make sure the privacy controls are all disabled in the UI.
1227 controls = [
1228 "Public", "Public Security", "Private Security", "Private",
1229 "Proprietary", "Embargoed"]
1230 self.assertTrue(
1231 all(browser.getControl(i, index=0).disabled for i in controls))
1232 expected_msg = (
1233 "The information type is being changed. The operation needs to "
1234 "finish before you can change it again.")
1235 self.assertIn(expected_msg, extract_text(browser.contents))
1236
1237 # Trying to change should have no effect in the backend, since the
1238 # repository is already changing info type and this field is read-only.
1239 browser.getControl("Private", index=1).click()
1240 browser.getControl("Change Git Repository").click()
1241 with person_logged_in(person):
1242 self.assertTrue(repository.pending_change_information_type)
1243 # It should have the same job it already had.
1244 job_util = getUtility(
1245 IGitRepositoryChangeInformationTypeJobSource)
1246 new_jobs = list(job_util.iterReady())
1247 self.assertEqual(jobs, new_jobs)
11901248
1191 def test_edit_view_ajax_render(self):1249 def test_edit_view_ajax_render(self):
1192 # An information type change request is processed as expected when1250 # An information type change request is processed as expected when
@@ -1208,8 +1266,16 @@ class TestGitRepositoryEditView(TestCaseWithFactory):
1208 person, repository.target, repository, view]1266 person, repository.target, repository, view]
1209 result = view.render()1267 result = view.render()
1210 self.assertEqual("", result)1268 self.assertEqual("", result)
1269 self.assertTrue(repository.pending_change_information_type)
1270 job_util = getUtility(
1271 IGitRepositoryChangeInformationTypeJobSource)
1272 jobs = list(job_util.iterReady())
1273 self.assertEqual(1, len(jobs))
1274 job = removeSecurityProxy(jobs[0])
1275 self.assertEqual(repository, job.repository)
1211 self.assertEqual(1276 self.assertEqual(
1212 repository.information_type, InformationType.PUBLICSECURITY)1277 InformationType.PUBLICSECURITY, job.information_type)
1278 self.assertEqual(person, job.user)
12131279
1214 def test_change_default_branch(self):1280 def test_change_default_branch(self):
1215 # An authorised user can change the default branch to one that1281 # An authorised user can change the default branch to one that
diff --git a/lib/lp/code/configure.zcml b/lib/lp/code/configure.zcml
index 898e645..3b03c7d 100644
--- a/lib/lp/code/configure.zcml
+++ b/lib/lp/code/configure.zcml
@@ -1111,6 +1111,11 @@
1111 provides="lp.code.interfaces.gitjob.IGitRepositoryModifiedMailJobSource">1111 provides="lp.code.interfaces.gitjob.IGitRepositoryModifiedMailJobSource">
1112 <allow interface="lp.code.interfaces.gitjob.IGitRepositoryModifiedMailJobSource" />1112 <allow interface="lp.code.interfaces.gitjob.IGitRepositoryModifiedMailJobSource" />
1113 </securedutility>1113 </securedutility>
1114 <securedutility
1115 component="lp.code.model.gitjob.GitRepositoryChangeInformationTypeJob"
1116 provides="lp.code.interfaces.gitjob.IGitRepositoryChangeInformationTypeJobSource">
1117 <allow interface="lp.code.interfaces.gitjob.IGitRepositoryChangeInformationTypeJobSource" />
1118 </securedutility>
1114 <class class="lp.code.model.gitjob.GitRefScanJob">1119 <class class="lp.code.model.gitjob.GitRefScanJob">
1115 <allow interface="lp.code.interfaces.gitjob.IGitJob" />1120 <allow interface="lp.code.interfaces.gitjob.IGitJob" />
1116 <allow interface="lp.code.interfaces.gitjob.IGitRefScanJob" />1121 <allow interface="lp.code.interfaces.gitjob.IGitRefScanJob" />
@@ -1123,6 +1128,10 @@
1123 <allow interface="lp.code.interfaces.gitjob.IGitJob" />1128 <allow interface="lp.code.interfaces.gitjob.IGitJob" />
1124 <allow interface="lp.code.interfaces.gitjob.IGitRepositoryModifiedMailJob" />1129 <allow interface="lp.code.interfaces.gitjob.IGitRepositoryModifiedMailJob" />
1125 </class>1130 </class>
1131 <class class="lp.code.model.gitjob.GitRepositoryChangeInformationTypeJob">
1132 <allow interface="lp.code.interfaces.gitjob.IGitJob" />
1133 <allow interface="lp.code.interfaces.gitjob.IGitRepositoryChangeInformationTypeJob" />
1134 </class>
11261135
1127 <lp:help-folder folder="help" name="+help-code" />1136 <lp:help-folder folder="help" name="+help-code" />
11281137
diff --git a/lib/lp/code/interfaces/gitjob.py b/lib/lp/code/interfaces/gitjob.py
index 4f31b19..4bbca04 100644
--- a/lib/lp/code/interfaces/gitjob.py
+++ b/lib/lp/code/interfaces/gitjob.py
@@ -1,4 +1,4 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the1# Copyright 2015-2020 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""GitJob interfaces."""4"""GitJob interfaces."""
@@ -11,6 +11,8 @@ __all__ = [
11 'IGitRefScanJobSource',11 'IGitRefScanJobSource',
12 'IGitRepositoryModifiedMailJob',12 'IGitRepositoryModifiedMailJob',
13 'IGitRepositoryModifiedMailJobSource',13 'IGitRepositoryModifiedMailJobSource',
14 'IGitRepositoryChangeInformationTypeJob',
15 'IGitRepositoryChangeInformationTypeJobSource',
14 'IReclaimGitRepositorySpaceJob',16 'IReclaimGitRepositorySpaceJob',
15 'IReclaimGitRepositorySpaceJobSource',17 'IReclaimGitRepositorySpaceJobSource',
16 ]18 ]
@@ -93,3 +95,20 @@ class IGitRepositoryModifiedMailJobSource(IJobSource):
93 :param repository_delta: An `IGitRepositoryDelta` describing the95 :param repository_delta: An `IGitRepositoryDelta` describing the
94 changes.96 changes.
95 """97 """
98
99
100class IGitRepositoryChangeInformationTypeJob(IRunnableJob):
101 """A Job to change repository's information type."""
102
103
104class IGitRepositoryChangeInformationTypeJobSource(IJobSource):
105
106 def create(repository, user, information_type, verify_policy=True):
107 """Create a job to change git repository's information type.
108
109 :param repository: The `IGitRepository` that was modified.
110 :param information_type: The `InformationType` to transition to.
111 :param user: The `IPerson` who is making the change.
112 :param verify_policy: Check if the new information type complies
113 with the `IGitNamespacePolicy`.
114 """
diff --git a/lib/lp/code/interfaces/gitrepository.py b/lib/lp/code/interfaces/gitrepository.py
index 192133a..dac0e8f 100644
--- a/lib/lp/code/interfaces/gitrepository.py
+++ b/lib/lp/code/interfaces/gitrepository.py
@@ -582,6 +582,10 @@ class IGitRepositoryView(IHasRecipes):
582 "Whether there are recent changes in this repository that have not "582 "Whether there are recent changes in this repository that have not "
583 "yet been scanned.")583 "yet been scanned.")
584584
585 pending_change_information_type = Attribute(
586 "Whether there is a pending request to change the information type "
587 "of this repository that have not been processed yet.")
588
585 def updateMergeCommitIDs(paths):589 def updateMergeCommitIDs(paths):
586 """Update commit SHA1s of merge proposals for this repository.590 """Update commit SHA1s of merge proposals for this repository.
587591
diff --git a/lib/lp/code/model/gitjob.py b/lib/lp/code/model/gitjob.py
index 1eb87da..f1e3d2a 100644
--- a/lib/lp/code/model/gitjob.py
+++ b/lib/lp/code/model/gitjob.py
@@ -33,10 +33,12 @@ from zope.interface import (
33 provider,33 provider,
34 )34 )
3535
36from lp.app.enums import InformationType
36from lp.app.errors import NotFoundError37from lp.app.errors import NotFoundError
37from lp.code.enums import (38from lp.code.enums import (
38 GitActivityType,39 GitActivityType,
39 GitPermissionType,40 GitPermissionType,
41 GitRepositoryStatus,
40 )42 )
41from lp.code.interfaces.githosting import IGitHostingClient43from lp.code.interfaces.githosting import IGitHostingClient
42from lp.code.interfaces.gitjob import (44from lp.code.interfaces.gitjob import (
@@ -45,6 +47,8 @@ from lp.code.interfaces.gitjob import (
45 IGitRefScanJobSource,47 IGitRefScanJobSource,
46 IGitRepositoryModifiedMailJob,48 IGitRepositoryModifiedMailJob,
47 IGitRepositoryModifiedMailJobSource,49 IGitRepositoryModifiedMailJobSource,
50 IGitRepositoryChangeInformationTypeJob,
51 IGitRepositoryChangeInformationTypeJobSource,
48 IReclaimGitRepositorySpaceJob,52 IReclaimGitRepositorySpaceJob,
49 IReclaimGitRepositorySpaceJobSource,53 IReclaimGitRepositorySpaceJobSource,
50 )54 )
@@ -100,6 +104,13 @@ class GitJobType(DBEnumeratedType):
100 modifications.104 modifications.
101 """)105 """)
102106
107 REPOSITORY_TRANSITION_TO_INFO_TYPE = DBItem(3, """
108 Change repository's information type
109
110 This job runs when a user requests to change privacy settings of a
111 repository.
112 """)
113
103114
104@implementer(IGitJob)115@implementer(IGitJob)
105class GitJob(StormBase):116class GitJob(StormBase):
@@ -393,3 +404,49 @@ class GitRepositoryModifiedMailJob(GitJobDerived):
393 def run(self):404 def run(self):
394 """See `IGitRepositoryModifiedMailJob`."""405 """See `IGitRepositoryModifiedMailJob`."""
395 self.getMailer().sendAll()406 self.getMailer().sendAll()
407
408
409@implementer(IGitRepositoryChangeInformationTypeJob)
410@provider(IGitRepositoryChangeInformationTypeJobSource)
411class GitRepositoryChangeInformationTypeJob(GitJobDerived):
412 """A Job to change git repository's information type."""
413
414 class_job_type = GitJobType.REPOSITORY_TRANSITION_TO_INFO_TYPE
415
416 config = config.IGitRepositoryChangeInformationTypeJobSource
417
418 @classmethod
419 def create(cls, repository, information_type, user, verify_policy=True):
420 """See `IGitRepositoryChangeInformationTypeJobSource`."""
421 metadata = {
422 "user": user.id,
423 "information_type": information_type.value,
424 "verify_policy": verify_policy,
425 }
426 git_job = GitJob(repository, cls.class_job_type, metadata)
427 job = cls(git_job)
428 job.celeryRunOnCommit()
429 return job
430
431 @property
432 def user(self):
433 return getUtility(IPersonSet).get(self.metadata["user"])
434
435 @property
436 def verify_policy(self):
437 return self.metadata["verify_policy"]
438
439 @property
440 def information_type(self):
441 return InformationType.items[self.metadata["information_type"]]
442
443 def run(self):
444 """See `IGitRepositoryChangeInformationTypeJob`."""
445 if not self.repository.pending_change_information_type:
446 raise AttributeError(
447 "The repository %s is not pending information type change." %
448 self.repository)
449
450 self.repository._transitionToInformationType(
451 self.information_type, self.user, self.verify_policy)
452 self.repository.status = GitRepositoryStatus.AVAILABLE
diff --git a/lib/lp/code/model/gitref.py b/lib/lp/code/model/gitref.py
index f7dc142..e2f788d 100644
--- a/lib/lp/code/model/gitref.py
+++ b/lib/lp/code/model/gitref.py
@@ -180,7 +180,7 @@ class GitRefMixin:
180180
181 def transitionToInformationType(self, information_type, user,181 def transitionToInformationType(self, information_type, user,
182 verify_policy=True):182 verify_policy=True):
183 return self.repository.transitionToInformationType(183 return self.repository._transitionToInformationType(
184 information_type, user, verify_policy=verify_policy)184 information_type, user, verify_policy=verify_policy)
185185
186 @property186 @property
diff --git a/lib/lp/code/model/gitrepository.py b/lib/lp/code/model/gitrepository.py
index fb5e703..d5bd382 100644
--- a/lib/lp/code/model/gitrepository.py
+++ b/lib/lp/code/model/gitrepository.py
@@ -117,7 +117,10 @@ from lp.code.interfaces.gitcollection import (
117 IGitCollection,117 IGitCollection,
118 )118 )
119from lp.code.interfaces.githosting import IGitHostingClient119from lp.code.interfaces.githosting import IGitHostingClient
120from lp.code.interfaces.gitjob import IGitRefScanJobSource120from lp.code.interfaces.gitjob import (
121 IGitRefScanJobSource,
122 IGitRepositoryChangeInformationTypeJobSource,
123 )
121from lp.code.interfaces.gitlookup import IGitLookup124from lp.code.interfaces.gitlookup import IGitLookup
122from lp.code.interfaces.gitnamespace import (125from lp.code.interfaces.gitnamespace import (
123 get_git_namespace,126 get_git_namespace,
@@ -882,6 +885,28 @@ class GitRepository(StormBase, WebhookTargetMixin, GitIdentityMixin):
882 def transitionToInformationType(self, information_type, user,885 def transitionToInformationType(self, information_type, user,
883 verify_policy=True):886 verify_policy=True):
884 """See `IGitRepository`."""887 """See `IGitRepository`."""
888 error_msg = None
889 if self.status != GitRepositoryStatus.AVAILABLE:
890 error_msg = ("Cannot change privacy settings while git "
891 "repository is being created.")
892 elif self.pending_change_information_type:
893 error_msg = ("Cannot change privacy settings while git repository "
894 "is pending another information type change.")
895 if error_msg is not None:
896 raise CannotChangeInformationType(error_msg)
897 util = getUtility(
898 IGitRepositoryChangeInformationTypeJobSource)
899 return util.create(self, information_type, user, verify_policy)
900
901 def _transitionToInformationType(self, information_type, user,
902 verify_policy=True):
903 """Synchronously make the change in this repository's information
904 type.
905
906 External callers should use the async, public version of this
907 method, since it deals with the side effects of changing
908 repository's privacy changes.
909 """
885 if self.information_type == information_type:910 if self.information_type == information_type:
886 return911 return
887 if (verify_policy and912 if (verify_policy and
@@ -1120,21 +1145,29 @@ class GitRepository(StormBase, WebhookTargetMixin, GitIdentityMixin):
1120 """See `IGitRepository`."""1145 """See `IGitRepository`."""
1121 return self.namespace.areRepositoriesMergeable(self, other)1146 return self.namespace.areRepositoriesMergeable(self, other)
11221147
1123 @property1148 def hasPendingJob(self, job_type):
1124 def pending_updates(self):1149 from lp.code.model.gitjob import GitJob
1125 """See `IGitRepository`."""
1126 from lp.code.model.gitjob import (
1127 GitJob,
1128 GitJobType,
1129 )
1130 jobs = Store.of(self).find(1150 jobs = Store.of(self).find(
1131 GitJob,1151 GitJob,
1132 GitJob.repository == self,1152 GitJob.repository == self,
1133 GitJob.job_type == GitJobType.REF_SCAN,1153 GitJob.job_type == job_type,
1134 GitJob.job == Job.id,1154 GitJob.job == Job.id,
1135 Job._status.is_in([JobStatus.WAITING, JobStatus.RUNNING]))1155 Job._status.is_in([JobStatus.WAITING, JobStatus.RUNNING]))
1136 return not jobs.is_empty()1156 return not jobs.is_empty()
11371157
1158 @property
1159 def pending_updates(self):
1160 """See `IGitRepository`."""
1161 from lp.code.model.gitjob import GitJobType
1162 return self.hasPendingJob(GitJobType.REF_SCAN)
1163
1164 @property
1165 def pending_change_information_type(self):
1166 """See `IGitRepository`."""
1167 from lp.code.model.gitjob import GitJobType
1168 return self.hasPendingJob(
1169 GitJobType.REPOSITORY_TRANSITION_TO_INFO_TYPE)
1170
1138 def updateMergeCommitIDs(self, paths):1171 def updateMergeCommitIDs(self, paths):
1139 """See `IGitRepository`."""1172 """See `IGitRepository`."""
1140 store = Store.of(self)1173 store = Store.of(self)
diff --git a/lib/lp/code/model/tests/test_gitcollection.py b/lib/lp/code/model/tests/test_gitcollection.py
index 447fcb4..372a633 100644
--- a/lib/lp/code/model/tests/test_gitcollection.py
+++ b/lib/lp/code/model/tests/test_gitcollection.py
@@ -599,7 +599,7 @@ class TestBranchMergeProposals(TestCaseWithFactory):
599 registrant = self.factory.makePerson()599 registrant = self.factory.makePerson()
600 mp1 = self.factory.makeBranchMergeProposalForGit(registrant=registrant)600 mp1 = self.factory.makeBranchMergeProposalForGit(registrant=registrant)
601 naked_repository = removeSecurityProxy(mp1.target_git_repository)601 naked_repository = removeSecurityProxy(mp1.target_git_repository)
602 naked_repository.transitionToInformationType(602 naked_repository._transitionToInformationType(
603 InformationType.USERDATA, registrant, verify_policy=False)603 InformationType.USERDATA, registrant, verify_policy=False)
604 collection = self.all_repositories.visibleByUser(None)604 collection = self.all_repositories.visibleByUser(None)
605 proposals = collection.getMergeProposals()605 proposals = collection.getMergeProposals()
diff --git a/lib/lp/code/model/tests/test_gitjob.py b/lib/lp/code/model/tests/test_gitjob.py
index 3f606c0..b4487b9 100644
--- a/lib/lp/code/model/tests/test_gitjob.py
+++ b/lib/lp/code/model/tests/test_gitjob.py
@@ -25,13 +25,16 @@ from testtools.matchers import (
25 MatchesStructure,25 MatchesStructure,
26 )26 )
27import transaction27import transaction
28from zope.component import getUtility
28from zope.interface import providedBy29from zope.interface import providedBy
29from zope.security.proxy import removeSecurityProxy30from zope.security.proxy import removeSecurityProxy
3031
32from lp.app.enums import InformationType
31from lp.code.adapters.gitrepository import GitRepositoryDelta33from lp.code.adapters.gitrepository import GitRepositoryDelta
32from lp.code.enums import (34from lp.code.enums import (
33 GitGranteeType,35 GitGranteeType,
34 GitObjectType,36 GitObjectType,
37 GitRepositoryStatus,
35 )38 )
36from lp.code.interfaces.branchmergeproposal import (39from lp.code.interfaces.branchmergeproposal import (
37 BRANCH_MERGE_PROPOSAL_WEBHOOKS_FEATURE_FLAG,40 BRANCH_MERGE_PROPOSAL_WEBHOOKS_FEATURE_FLAG,
@@ -39,6 +42,7 @@ from lp.code.interfaces.branchmergeproposal import (
39from lp.code.interfaces.gitjob import (42from lp.code.interfaces.gitjob import (
40 IGitJob,43 IGitJob,
41 IGitRefScanJob,44 IGitRefScanJob,
45 IGitRepositoryChangeInformationTypeJobSource,
42 IReclaimGitRepositorySpaceJob,46 IReclaimGitRepositorySpaceJob,
43 )47 )
44from lp.code.model.gitjob import (48from lp.code.model.gitjob import (
@@ -47,9 +51,11 @@ from lp.code.model.gitjob import (
47 GitJobDerived,51 GitJobDerived,
48 GitJobType,52 GitJobType,
49 GitRefScanJob,53 GitRefScanJob,
54 GitRepositoryChangeInformationTypeJob,
50 ReclaimGitRepositorySpaceJob,55 ReclaimGitRepositorySpaceJob,
51 )56 )
52from lp.code.tests.helpers import GitHostingFixture57from lp.code.tests.helpers import GitHostingFixture
58from lp.registry.errors import CannotChangeInformationType
53from lp.services.config import config59from lp.services.config import config
54from lp.services.database.constants import UTC_NOW60from lp.services.database.constants import UTC_NOW
55from lp.services.features.testing import FeatureFixture61from lp.services.features.testing import FeatureFixture
@@ -362,6 +368,59 @@ class TestReclaimGitRepositorySpaceJob(TestCaseWithFactory):
362 self.assertEqual([(path,)], hosting_fixture.delete.extract_args())368 self.assertEqual([(path,)], hosting_fixture.delete.extract_args())
363369
364370
371class TestGitRepositoryTransitionInformationType(TestCaseWithFactory):
372
373 layer = ZopelessDatabaseLayer
374
375 def test_block_multiple_requests_to_change_info_type(self):
376 repo = self.factory.makeGitRepository()
377 repo.transitionToInformationType(
378 InformationType.PRIVATESECURITY, repo.owner)
379 self.assertTrue(repo.pending_change_information_type)
380 expected_msg = (
381 "Cannot change privacy settings while git repository is "
382 "pending another information type change.")
383 self.assertRaisesRegex(
384 CannotChangeInformationType, expected_msg,
385 repo.transitionToInformationType,
386 InformationType.PROPRIETARY, repo.owner)
387
388 def test_avoid_transitioning_while_creating(self):
389 repo = self.factory.makeGitRepository()
390 removeSecurityProxy(repo).status = GitRepositoryStatus.CREATING
391 expected_msg = (
392 "Cannot change privacy settings while git repository is "
393 "being created.")
394 self.assertRaisesRegex(
395 CannotChangeInformationType, expected_msg,
396 repo.transitionToInformationType,
397 InformationType.PROPRIETARY, repo.owner)
398
399 def test_run_changes_info_type(self):
400 repo = self.factory.makeGitRepository(
401 information_type=InformationType.PUBLIC)
402 # Change to a private info type and with verify_policy, so we hit as
403 # many database tables as possible.
404 repo.transitionToInformationType(
405 InformationType.PRIVATESECURITY, repo.owner, verify_policy=True)
406 self.assertTrue(repo.pending_change_information_type)
407
408 job_util = getUtility(
409 IGitRepositoryChangeInformationTypeJobSource)
410 jobs = list(job_util.iterReady())
411 self.assertEqual(1, len(jobs))
412 with dbuser(GitRepositoryChangeInformationTypeJob.config.dbuser):
413 JobRunner(jobs).runAll()
414
415 self.assertEqual(GitRepositoryStatus.AVAILABLE, repo.status)
416 self.assertEqual(
417 InformationType.PRIVATESECURITY, repo.information_type)
418
419 # After the job finished, another change is possible.
420 repo.transitionToInformationType(InformationType.PUBLIC, repo.owner)
421 self.assertTrue(repo.pending_change_information_type)
422
423
365class TestDescribeRepositoryDelta(TestCaseWithFactory):424class TestDescribeRepositoryDelta(TestCaseWithFactory):
366 """Tests for `describe_repository_delta`."""425 """Tests for `describe_repository_delta`."""
367426
diff --git a/lib/lp/code/model/tests/test_gitrepository.py b/lib/lp/code/model/tests/test_gitrepository.py
index b83f9da..f0ba2b4 100644
--- a/lib/lp/code/model/tests/test_gitrepository.py
+++ b/lib/lp/code/model/tests/test_gitrepository.py
@@ -809,7 +809,7 @@ class TestGitRepositoryDeletion(TestCaseWithFactory):
809809
810 def test_private_subscription_does_not_disable_deletion(self):810 def test_private_subscription_does_not_disable_deletion(self):
811 # A private repository that has a subscription can be deleted.811 # A private repository that has a subscription can be deleted.
812 self.repository.transitionToInformationType(812 removeSecurityProxy(self.repository)._transitionToInformationType(
813 InformationType.USERDATA, self.repository.owner,813 InformationType.USERDATA, self.repository.owner,
814 verify_policy=False)814 verify_policy=False)
815 self.repository.subscribe(815 self.repository.subscribe(
@@ -2054,8 +2054,7 @@ class TestGitRepositoryModerate(TestCaseWithFactory):
2054 project.setBranchSharingPolicy(BranchSharingPolicy.PUBLIC)2054 project.setBranchSharingPolicy(BranchSharingPolicy.PUBLIC)
2055 repository.transitionToInformationType(2055 repository.transitionToInformationType(
2056 InformationType.PRIVATESECURITY, project.owner)2056 InformationType.PRIVATESECURITY, project.owner)
2057 self.assertEqual(2057 self.assertTrue(repository.pending_change_information_type)
2058 InformationType.PRIVATESECURITY, repository.information_type)
20592058
2060 def test_attribute_smoketest(self):2059 def test_attribute_smoketest(self):
2061 # Users with launchpad.Moderate can set attributes.2060 # Users with launchpad.Moderate can set attributes.
@@ -3454,7 +3453,7 @@ class TestGitRepositorySet(TestCaseWithFactory):
3454 ]3453 ]
3455 for repository, modified_date in zip(repositories, modified_dates):3454 for repository, modified_date in zip(repositories, modified_dates):
3456 removeSecurityProxy(repository).date_last_modified = modified_date3455 removeSecurityProxy(repository).date_last_modified = modified_date
3457 removeSecurityProxy(repositories[0]).transitionToInformationType(3456 removeSecurityProxy(repositories[0])._transitionToInformationType(
3458 InformationType.PRIVATESECURITY, repositories[0].registrant)3457 InformationType.PRIVATESECURITY, repositories[0].registrant)
3459 self.assertEqual(3458 self.assertEqual(
3460 [repositories[3], repositories[4], repositories[1],3459 [repositories[3], repositories[4], repositories[1],
@@ -3966,8 +3965,7 @@ class TestGitRepositoryWebservice(TestCaseWithFactory):
3966 json.dumps({"information_type": "Public Security"}))3965 json.dumps({"information_type": "Public Security"}))
3967 self.assertEqual(209, response.status)3966 self.assertEqual(209, response.status)
3968 with person_logged_in(ANONYMOUS):3967 with person_logged_in(ANONYMOUS):
3969 self.assertEqual(3968 self.assertTrue(repository_db.pending_change_information_type)
3970 InformationType.PUBLICSECURITY, repository_db.information_type)
39713969
3972 def test_set_information_type_other_person(self):3970 def test_set_information_type_other_person(self):
3973 # An unrelated user cannot change the information type.3971 # An unrelated user cannot change the information type.
diff --git a/lib/lp/code/xmlrpc/tests/test_git.py b/lib/lp/code/xmlrpc/tests/test_git.py
index dcbcee6..e884cd2 100644
--- a/lib/lp/code/xmlrpc/tests/test_git.py
+++ b/lib/lp/code/xmlrpc/tests/test_git.py
@@ -899,7 +899,7 @@ class TestGitAPI(TestGitAPIMixin, TestCaseWithFactory):
899 for _ in range(2)]899 for _ in range(2)]
900 private_repository = code_imports[0].git_repository900 private_repository = code_imports[0].git_repository
901 removeSecurityProxy(901 removeSecurityProxy(
902 private_repository).transitionToInformationType(902 private_repository)._transitionToInformationType(
903 InformationType.PRIVATESECURITY, private_repository.owner)903 InformationType.PRIVATESECURITY, private_repository.owner)
904 with celebrity_logged_in("vcs_imports"):904 with celebrity_logged_in("vcs_imports"):
905 jobs = [905 jobs = [
@@ -1077,7 +1077,7 @@ class TestGitAPI(TestGitAPIMixin, TestCaseWithFactory):
1077 for _ in range(2)]1077 for _ in range(2)]
1078 private_repository = code_imports[0].git_repository1078 private_repository = code_imports[0].git_repository
1079 removeSecurityProxy(1079 removeSecurityProxy(
1080 private_repository).transitionToInformationType(1080 private_repository)._transitionToInformationType(
1081 InformationType.PRIVATESECURITY, private_repository.owner)1081 InformationType.PRIVATESECURITY, private_repository.owner)
1082 with celebrity_logged_in("vcs_imports"):1082 with celebrity_logged_in("vcs_imports"):
1083 jobs = [1083 jobs = [
@@ -1687,7 +1687,7 @@ class TestGitAPI(TestGitAPIMixin, TestCaseWithFactory):
1687 target_rcs_type=TargetRevisionControlSystems.GIT)1687 target_rcs_type=TargetRevisionControlSystems.GIT)
1688 for _ in range(2)]1688 for _ in range(2)]
1689 private_repository = code_imports[0].git_repository1689 private_repository = code_imports[0].git_repository
1690 removeSecurityProxy(private_repository).transitionToInformationType(1690 removeSecurityProxy(private_repository)._transitionToInformationType(
1691 InformationType.PRIVATESECURITY, private_repository.owner)1691 InformationType.PRIVATESECURITY, private_repository.owner)
1692 with celebrity_logged_in("vcs_imports"):1692 with celebrity_logged_in("vcs_imports"):
1693 jobs = [1693 jobs = [
@@ -2012,7 +2012,7 @@ class TestGitAPI(TestGitAPIMixin, TestCaseWithFactory):
2012 target_rcs_type=TargetRevisionControlSystems.GIT)2012 target_rcs_type=TargetRevisionControlSystems.GIT)
2013 for _ in range(2)]2013 for _ in range(2)]
2014 private_repository = code_imports[0].git_repository2014 private_repository = code_imports[0].git_repository
2015 removeSecurityProxy(private_repository).transitionToInformationType(2015 removeSecurityProxy(private_repository)._transitionToInformationType(
2016 InformationType.PRIVATESECURITY, private_repository.owner)2016 InformationType.PRIVATESECURITY, private_repository.owner)
2017 with celebrity_logged_in("vcs_imports"):2017 with celebrity_logged_in("vcs_imports"):
2018 jobs = [2018 jobs = [
@@ -2268,7 +2268,7 @@ class TestGitAPI(TestGitAPIMixin, TestCaseWithFactory):
2268 target_rcs_type=TargetRevisionControlSystems.GIT)2268 target_rcs_type=TargetRevisionControlSystems.GIT)
2269 for _ in range(2)]2269 for _ in range(2)]
2270 private_repository = code_imports[0].git_repository2270 private_repository = code_imports[0].git_repository
2271 removeSecurityProxy(private_repository).transitionToInformationType(2271 removeSecurityProxy(private_repository)._transitionToInformationType(
2272 InformationType.PRIVATESECURITY, private_repository.owner)2272 InformationType.PRIVATESECURITY, private_repository.owner)
2273 with celebrity_logged_in("vcs_imports"):2273 with celebrity_logged_in("vcs_imports"):
2274 jobs = [2274 jobs = [
diff --git a/lib/lp/services/config/schema-lazr.conf b/lib/lp/services/config/schema-lazr.conf
index aaa9d30..1157cb5 100644
--- a/lib/lp/services/config/schema-lazr.conf
+++ b/lib/lp/services/config/schema-lazr.conf
@@ -1768,6 +1768,7 @@ job_sources:
1768 ICommercialExpiredJobSource,1768 ICommercialExpiredJobSource,
1769 IExpiringMembershipNotificationJobSource,1769 IExpiringMembershipNotificationJobSource,
1770 IGitRepositoryModifiedMailJobSource,1770 IGitRepositoryModifiedMailJobSource,
1771 IGitRepositoryChangeInformationTypeJobSource,
1771 IMembershipNotificationJobSource,1772 IMembershipNotificationJobSource,
1772 IOCIRecipeRequestBuildsJobSource,1773 IOCIRecipeRequestBuildsJobSource,
1773 IOCIRegistryUploadJobSource,1774 IOCIRegistryUploadJobSource,
@@ -1829,6 +1830,11 @@ module: lp.code.interfaces.gitjob
1829dbuser: send-branch-mail1830dbuser: send-branch-mail
1830crontab_group: MAIN1831crontab_group: MAIN
18311832
1833[IGitRepositoryChangeInformationTypeJobSource]
1834module: lp.code.interfaces.gitjob
1835dbuser: privacy-change-jobs
1836crontab_group: MAIN
1837
1832[IInitializeDistroSeriesJobSource]1838[IInitializeDistroSeriesJobSource]
1833module: lp.soyuz.interfaces.distributionjob1839module: lp.soyuz.interfaces.distributionjob
1834dbuser: initializedistroseries1840dbuser: initializedistroseries
diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
index 57babc5..9d1f53b 100644
--- a/lib/lp/testing/factory.py
+++ b/lib/lp/testing/factory.py
@@ -1815,7 +1815,7 @@ class BareLaunchpadObjectFactory(ObjectFactory):
1815 reviewer=reviewer, **optional_repository_args)1815 reviewer=reviewer, **optional_repository_args)
1816 naked_repository = removeSecurityProxy(repository)1816 naked_repository = removeSecurityProxy(repository)
1817 if information_type is not None:1817 if information_type is not None:
1818 naked_repository.transitionToInformationType(1818 naked_repository._transitionToInformationType(
1819 information_type, registrant, verify_policy=False)1819 information_type, registrant, verify_policy=False)
1820 return repository1820 return repository
18211821