Merge ~pappacena/launchpad:mp-refs-privacy-change into launchpad:master

Proposed by Thiago F. Pappacena
Status: Needs review
Proposed branch: ~pappacena/launchpad:mp-refs-privacy-change
Merge into: launchpad:master
Prerequisite: ~pappacena/launchpad:create-mp-refs
Diff against target: 582 lines (+309/-16)
10 files modified
lib/lp/code/configure.zcml (+9/-0)
lib/lp/code/interfaces/gitjob.py (+16/-1)
lib/lp/code/model/branchmergeproposal.py (+9/-7)
lib/lp/code/model/gitjob.py (+63/-1)
lib/lp/code/model/gitrepository.py (+11/-1)
lib/lp/code/model/tests/test_branchmergeproposal.py (+4/-4)
lib/lp/code/model/tests/test_gitjob.py (+127/-1)
lib/lp/code/model/tests/test_gitrepository.py (+62/-0)
lib/lp/services/config/schema-lazr.conf (+4/-0)
lib/lp/testing/fakemethod.py (+4/-1)
Reviewer Review Type Date Requested Status
Colin Watson (community) Needs Information
Review via email: mp+390940@code.launchpad.net

Commit message

Background job to add/remove merge proposal's virtual refs when a repository has its privacy changed

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote :

There's an unfortunate complication around private-to-public transitions that I fear is going to require quite a bit more work to get right (assuming I haven't got the wrong end of the stick somewhere). Details below.

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

Added one comment about the complication on private-to-public, just to make sure I understood the scenario. Meanwhile, I'll work out the other details in the MP.

Revision history for this message
Colin Watson (cjwatson) :

Unmerged commits

dcdb5f4... by Thiago F. Pappacena

Merge branch 'create-mp-refs' into mp-refs-privacy-change

f3bda00... by Thiago F. Pappacena

Fixing test

9a2f2f7... by Thiago F. Pappacena

Retry strategy for virtual refs sync job

cca2a0f... by Thiago F. Pappacena

Merge branch 'create-mp-refs' into mp-refs-privacy-change

2ca86e6... by Thiago F. Pappacena

Fixing test

ebd11bb... by Thiago F. Pappacena

Adding job to reconcile mp virtual refs

3c2ffb2... by Thiago F. Pappacena

Trigger MP reconciliation job when changing repository privacy setting

30c88c7... by Thiago F. Pappacena

Merge branch 'githosting-copy-and-delete-refs' into create-mp-refs

7e11cb0... by Thiago F. Pappacena

Refactoring virtual refs creation and preparing privacy code

1f7f371... by Thiago F. Pappacena

Adding feature flag and triggering copy on MP creation

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/lib/lp/code/configure.zcml b/lib/lp/code/configure.zcml
index 898e645..4adc8d0 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.GitRepositoryVirtualRefsSyncJob"
1116 provides="lp.code.interfaces.gitjob.IGitRepositoryVirtualRefsSyncJobSource">
1117 <allow interface="lp.code.interfaces.gitjob.IGitRepositoryVirtualRefsSyncJobSource" />
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.GitRepositoryVirtualRefsSyncJob">
1132 <allow interface="lp.code.interfaces.gitjob.IGitJob" />
1133 <allow interface="lp.code.interfaces.gitjob.IGitRepositoryVirtualRefsSyncJob" />
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..7c3c038 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 'IGitRepositoryVirtualRefsSyncJob',
15 'IGitRepositoryVirtualRefsSyncJobSource',
14 'IReclaimGitRepositorySpaceJob',16 'IReclaimGitRepositorySpaceJob',
15 'IReclaimGitRepositorySpaceJobSource',17 'IReclaimGitRepositorySpaceJobSource',
16 ]18 ]
@@ -93,3 +95,16 @@ 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 IGitRepositoryVirtualRefsSyncJob(IRunnableJob):
101 """A job to synchronize all MPs virtual refs related to this repository."""
102
103
104class IGitRepositoryVirtualRefsSyncJobSource(IJobSource):
105
106 def create(repository):
107 """Send email about repository modifications.
108
109 :param repository: The `IGitRepository` that needs sync.
110 """
diff --git a/lib/lp/code/model/branchmergeproposal.py b/lib/lp/code/model/branchmergeproposal.py
index 8a29a99..bd804a8 100644
--- a/lib/lp/code/model/branchmergeproposal.py
+++ b/lib/lp/code/model/branchmergeproposal.py
@@ -1247,7 +1247,7 @@ class BranchMergeProposal(SQLBase, BugLinkTargetMixin):
1247 self.target_git_repository.getInternalPath(),1247 self.target_git_repository.getInternalPath(),
1248 GIT_MP_VIRTUAL_REF_FORMAT.format(mp=self))]1248 GIT_MP_VIRTUAL_REF_FORMAT.format(mp=self))]
12491249
1250 def copyGitHostingVirtualRefs(self):1250 def copyGitHostingVirtualRefs(self, logger=logger):
1251 """Requests virtual refs copy operations on GitHosting in order to1251 """Requests virtual refs copy operations on GitHosting in order to
1252 keep them up-to-date with current MP's state.1252 keep them up-to-date with current MP's state.
12531253
@@ -1257,10 +1257,10 @@ class BranchMergeProposal(SQLBase, BugLinkTargetMixin):
1257 hosting_client = getUtility(IGitHostingClient)1257 hosting_client = getUtility(IGitHostingClient)
1258 hosting_client.copyRefs(1258 hosting_client.copyRefs(
1259 self.source_git_repository.getInternalPath(),1259 self.source_git_repository.getInternalPath(),
1260 copy_operations)1260 copy_operations, logger=logger)
1261 return copy_operations1261 return copy_operations
12621262
1263 def deleteGitHostingVirtualRefs(self, except_refs=None):1263 def deleteGitHostingVirtualRefs(self, except_refs=None, logger=None):
1264 """Deletes on git code hosting service all virtual refs, except1264 """Deletes on git code hosting service all virtual refs, except
1265 those ones in the given list."""1265 those ones in the given list."""
1266 if self.source_git_ref is None:1266 if self.source_git_ref is None:
@@ -1278,14 +1278,16 @@ class BranchMergeProposal(SQLBase, BugLinkTargetMixin):
1278 if ref not in (except_refs or []):1278 if ref not in (except_refs or []):
1279 hosting_client = getUtility(IGitHostingClient)1279 hosting_client = getUtility(IGitHostingClient)
1280 hosting_client.deleteRef(1280 hosting_client.deleteRef(
1281 self.target_git_repository.getInternalPath(), ref)1281 self.target_git_repository.getInternalPath(), ref,
1282 logger=logger)
12821283
1283 def syncGitHostingVirtualRefs(self):1284 def syncGitHostingVirtualRefs(self, logger=None):
1284 """Requests all copies and deletion of virtual refs to make git code1285 """Requests all copies and deletion of virtual refs to make git code
1285 hosting in sync with this MP."""1286 hosting in sync with this MP."""
1286 operations = self.copyGitHostingVirtualRefs()1287 operations = self.copyGitHostingVirtualRefs(logger=logger)
1287 copied_refs = [i.target_ref for i in operations]1288 copied_refs = [i.target_ref for i in operations]
1288 self.deleteGitHostingVirtualRefs(except_refs=copied_refs)1289 self.deleteGitHostingVirtualRefs(
1290 except_refs=copied_refs, logger=logger)
12891291
1290 def scheduleDiffUpdates(self, return_jobs=True):1292 def scheduleDiffUpdates(self, return_jobs=True):
1291 """See `IBranchMergeProposal`."""1293 """See `IBranchMergeProposal`."""
diff --git a/lib/lp/code/model/gitjob.py b/lib/lp/code/model/gitjob.py
index 3c041da..d845f59 100644
--- a/lib/lp/code/model/gitjob.py
+++ b/lib/lp/code/model/gitjob.py
@@ -1,4 +1,4 @@
1# Copyright 2015-2018 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__metaclass__ = type4__metaclass__ = type
@@ -45,6 +45,8 @@ from lp.code.interfaces.gitjob import (
45 IGitRefScanJobSource,45 IGitRefScanJobSource,
46 IGitRepositoryModifiedMailJob,46 IGitRepositoryModifiedMailJob,
47 IGitRepositoryModifiedMailJobSource,47 IGitRepositoryModifiedMailJobSource,
48 IGitRepositoryVirtualRefsSyncJob,
49 IGitRepositoryVirtualRefsSyncJobSource,
48 IReclaimGitRepositorySpaceJob,50 IReclaimGitRepositorySpaceJob,
49 IReclaimGitRepositorySpaceJobSource,51 IReclaimGitRepositorySpaceJobSource,
50 )52 )
@@ -100,6 +102,13 @@ class GitJobType(DBEnumeratedType):
100 modifications.102 modifications.
101 """)103 """)
102104
105 SYNC_MP_VIRTUAL_REFS = DBItem(3, """
106 Sync merge proposals virtual refs
107
108 This job runs against a repository to synchronize the virtual refs
109 from all merge proposals related to this repository.
110 """)
111
103112
104@implementer(IGitJob)113@implementer(IGitJob)
105class GitJob(StormBase):114class GitJob(StormBase):
@@ -404,3 +413,56 @@ class GitRepositoryModifiedMailJob(GitJobDerived):
404 def run(self):413 def run(self):
405 """See `IGitRepositoryModifiedMailJob`."""414 """See `IGitRepositoryModifiedMailJob`."""
406 self.getMailer().sendAll()415 self.getMailer().sendAll()
416
417
418@implementer(IGitRepositoryVirtualRefsSyncJob)
419@provider(IGitRepositoryVirtualRefsSyncJobSource)
420class GitRepositoryVirtualRefsSyncJob(GitJobDerived):
421 """A Job that scans a Git repository for its current list of references."""
422 class_job_type = GitJobType.SYNC_MP_VIRTUAL_REFS
423
424 class RetryException(Exception):
425 pass
426
427 max_retries = 5
428
429 retry_error_types = (RetryException, )
430
431 config = config.IGitRepositoryVirtualRefsSyncJobSource
432
433 @classmethod
434 def create(cls, repository):
435 metadata = {"synced_mp_ids": []}
436 git_job = GitJob(repository, cls.class_job_type, metadata)
437 job = cls(git_job)
438 job.celeryRunOnCommit()
439 return job
440
441 @property
442 def synced_mp_ids(self):
443 return self.metadata["synced_mp_ids"]
444
445 def add_synced_mp_id(self, mp_id):
446 self.metadata["synced_mp_ids"].append(mp_id)
447
448 def run(self):
449 log.info(
450 "Starting to re-sync virtual refs from repository %s",
451 self.repository)
452 failed = False
453 for mp in self.repository.landing_targets:
454 if mp.id in self.synced_mp_ids:
455 continue
456 try:
457 log.info("Re-syncing virtual refs from MP %s", mp)
458 mp.syncGitHostingVirtualRefs(logger=log)
459 self.synced_mp_ids.append(mp.id)
460 except Exception as e:
461 log.info(
462 "Re-syncing virtual refs from MP %s failed: %s", mp, e)
463 failed = True
464 log.info(
465 "Finished re-syncing virtual refs from repository %s%s",
466 self.repository, " with failures" if failed else "")
467 if failed:
468 raise GitRepositoryVirtualRefsSyncJob.RetryException()
diff --git a/lib/lp/code/model/gitrepository.py b/lib/lp/code/model/gitrepository.py
index e2451fa..6379fce 100644
--- a/lib/lp/code/model/gitrepository.py
+++ b/lib/lp/code/model/gitrepository.py
@@ -116,7 +116,10 @@ from lp.code.interfaces.gitcollection import (
116 IGitCollection,116 IGitCollection,
117 )117 )
118from lp.code.interfaces.githosting import IGitHostingClient118from lp.code.interfaces.githosting import IGitHostingClient
119from lp.code.interfaces.gitjob import IGitRefScanJobSource119from lp.code.interfaces.gitjob import (
120 IGitRefScanJobSource,
121 IGitRepositoryVirtualRefsSyncJobSource,
122 )
120from lp.code.interfaces.gitlookup import IGitLookup123from lp.code.interfaces.gitlookup import IGitLookup
121from lp.code.interfaces.gitnamespace import (124from lp.code.interfaces.gitnamespace import (
122 get_git_namespace,125 get_git_namespace,
@@ -866,6 +869,7 @@ class GitRepository(StormBase, WebhookTargetMixin, GitIdentityMixin):
866 raise CannotChangeInformationType("Forbidden by project policy.")869 raise CannotChangeInformationType("Forbidden by project policy.")
867 # XXX cjwatson 2019-03-29: Check privacy rules on snaps that use870 # XXX cjwatson 2019-03-29: Check privacy rules on snaps that use
868 # this repository.871 # this repository.
872 was_private = self.private
869 self.information_type = information_type873 self.information_type = information_type
870 self._reconcileAccess()874 self._reconcileAccess()
871 if (information_type in PRIVATE_INFORMATION_TYPES and875 if (information_type in PRIVATE_INFORMATION_TYPES and
@@ -883,6 +887,12 @@ class GitRepository(StormBase, WebhookTargetMixin, GitIdentityMixin):
883 # subscriptions.887 # subscriptions.
884 getUtility(IRemoveArtifactSubscriptionsJobSource).create(user, [self])888 getUtility(IRemoveArtifactSubscriptionsJobSource).create(user, [self])
885889
890 # If privacy changed, we need to re-sync all virtual refs from
891 # all MPs to avoid disclosing private code, or to add the virtual
892 # refs to the now public code.
893 if was_private != self.private:
894 getUtility(IGitRepositoryVirtualRefsSyncJobSource).create(self)
895
886 def setName(self, new_name, user):896 def setName(self, new_name, user):
887 """See `IGitRepository`."""897 """See `IGitRepository`."""
888 self.namespace.moveRepository(self, user, new_name=new_name)898 self.namespace.moveRepository(self, user, new_name=new_name)
diff --git a/lib/lp/code/model/tests/test_branchmergeproposal.py b/lib/lp/code/model/tests/test_branchmergeproposal.py
index 54cf516..277650c 100644
--- a/lib/lp/code/model/tests/test_branchmergeproposal.py
+++ b/lib/lp/code/model/tests/test_branchmergeproposal.py
@@ -285,7 +285,7 @@ class TestGitBranchMergeProposalVirtualRefs(TestCaseWithFactory):
285 self.assertEqual(1, self.hosting_fixture.copyRefs.call_count)285 self.assertEqual(1, self.hosting_fixture.copyRefs.call_count)
286 args, kwargs = self.hosting_fixture.copyRefs.calls[0]286 args, kwargs = self.hosting_fixture.copyRefs.calls[0]
287 arg_path, arg_copy_operations = args287 arg_path, arg_copy_operations = args
288 self.assertEqual({}, kwargs)288 self.assertEqual({'logger': None}, kwargs)
289 self.assertEqual(mp.source_git_repository.getInternalPath(), arg_path)289 self.assertEqual(mp.source_git_repository.getInternalPath(), arg_path)
290 self.assertEqual(1, len(arg_copy_operations))290 self.assertEqual(1, len(arg_copy_operations))
291 self.assertThat(arg_copy_operations[0], MatchesStructure(291 self.assertThat(arg_copy_operations[0], MatchesStructure(
@@ -357,7 +357,7 @@ class TestGitBranchMergeProposalVirtualRefs(TestCaseWithFactory):
357 # Check lp.code.subscribers.branchmergeproposal.merge_proposal_created.357 # Check lp.code.subscribers.branchmergeproposal.merge_proposal_created.
358 self.assertEqual(1, self.hosting_fixture.copyRefs.call_count)358 self.assertEqual(1, self.hosting_fixture.copyRefs.call_count)
359 args, kwargs = self.hosting_fixture.copyRefs.calls[0]359 args, kwargs = self.hosting_fixture.copyRefs.calls[0]
360 self.assertEquals({}, kwargs)360 self.assertEquals({'logger': None}, kwargs)
361 self.assertEqual(args[0], source_repo.getInternalPath())361 self.assertEqual(args[0], source_repo.getInternalPath())
362 self.assertEqual(1, len(args[1]))362 self.assertEqual(1, len(args[1]))
363 self.assertThat(args[1][0], MatchesStructure(363 self.assertThat(args[1][0], MatchesStructure(
@@ -384,7 +384,7 @@ class TestGitBranchMergeProposalVirtualRefs(TestCaseWithFactory):
384 self.assertEqual(0, self.hosting_fixture.copyRefs.call_count)384 self.assertEqual(0, self.hosting_fixture.copyRefs.call_count)
385 self.assertEqual(1, self.hosting_fixture.deleteRef.call_count)385 self.assertEqual(1, self.hosting_fixture.deleteRef.call_count)
386 args, kwargs = self.hosting_fixture.deleteRef.calls[0]386 args, kwargs = self.hosting_fixture.deleteRef.calls[0]
387 self.assertEqual({}, kwargs)387 self.assertEqual({'logger': None}, kwargs)
388 self.assertEqual(388 self.assertEqual(
389 (target_repo.getInternalPath(), "refs/merge/%s/head" % mp.id),389 (target_repo.getInternalPath(), "refs/merge/%s/head" % mp.id),
390 args)390 args)
@@ -1658,7 +1658,7 @@ class TestBranchMergeProposalDeletion(TestCaseWithFactory):
1658 args = hosting_fixture.deleteRef.calls[0]1658 args = hosting_fixture.deleteRef.calls[0]
1659 self.assertEqual((1659 self.assertEqual((
1660 (proposal.target_git_repository.getInternalPath(),1660 (proposal.target_git_repository.getInternalPath(),
1661 'refs/merge/%s/head' % proposal.id), {}), args)1661 'refs/merge/%s/head' % proposal.id), {'logger': None}), args)
16621662
16631663
1664class TestBranchMergeProposalBugs(WithVCSScenarios, TestCaseWithFactory):1664class TestBranchMergeProposalBugs(WithVCSScenarios, TestCaseWithFactory):
diff --git a/lib/lp/code/model/tests/test_gitjob.py b/lib/lp/code/model/tests/test_gitjob.py
index 5964944..c92be62 100644
--- a/lib/lp/code/model/tests/test_gitjob.py
+++ b/lib/lp/code/model/tests/test_gitjob.py
@@ -1,4 +1,4 @@
1# Copyright 2015-2019 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"""Tests for `GitJob`s."""4"""Tests for `GitJob`s."""
@@ -20,13 +20,16 @@ from testtools.matchers import (
20 ContainsDict,20 ContainsDict,
21 Equals,21 Equals,
22 MatchesDict,22 MatchesDict,
23 MatchesListwise,
23 MatchesSetwise,24 MatchesSetwise,
24 MatchesStructure,25 MatchesStructure,
25 )26 )
26import transaction27import transaction
28from zope.component import getUtility
27from zope.interface import providedBy29from zope.interface import providedBy
28from zope.security.proxy import removeSecurityProxy30from zope.security.proxy import removeSecurityProxy
2931
32from lp.app.enums import InformationType
30from lp.code.adapters.gitrepository import GitRepositoryDelta33from lp.code.adapters.gitrepository import GitRepositoryDelta
31from lp.code.enums import (34from lp.code.enums import (
32 GitGranteeType,35 GitGranteeType,
@@ -38,8 +41,10 @@ from lp.code.interfaces.branchmergeproposal import (
38from lp.code.interfaces.gitjob import (41from lp.code.interfaces.gitjob import (
39 IGitJob,42 IGitJob,
40 IGitRefScanJob,43 IGitRefScanJob,
44 IGitRepositoryVirtualRefsSyncJobSource,
41 IReclaimGitRepositorySpaceJob,45 IReclaimGitRepositorySpaceJob,
42 )46 )
47from lp.code.interfaces.gitrepository import GIT_CREATE_MP_VIRTUAL_REF
43from lp.code.model.gitjob import (48from lp.code.model.gitjob import (
44 describe_repository_delta,49 describe_repository_delta,
45 GitJob,50 GitJob,
@@ -49,9 +54,11 @@ from lp.code.model.gitjob import (
49 ReclaimGitRepositorySpaceJob,54 ReclaimGitRepositorySpaceJob,
50 )55 )
51from lp.code.tests.helpers import GitHostingFixture56from lp.code.tests.helpers import GitHostingFixture
57from lp.services.compat import mock
52from lp.services.config import config58from lp.services.config import config
53from lp.services.database.constants import UTC_NOW59from lp.services.database.constants import UTC_NOW
54from lp.services.features.testing import FeatureFixture60from lp.services.features.testing import FeatureFixture
61from lp.services.job.interfaces.job import JobStatus
55from lp.services.job.runner import JobRunner62from lp.services.job.runner import JobRunner
56from lp.services.utils import seconds_since_epoch63from lp.services.utils import seconds_since_epoch
57from lp.services.webapp import canonical_url64from lp.services.webapp import canonical_url
@@ -484,5 +491,124 @@ class TestDescribeRepositoryDelta(TestCaseWithFactory):
484 snapshot, repository)491 snapshot, repository)
485492
486493
494class TestGitRepositoryVirtualRefsSyncJob(TestCaseWithFactory):
495 """Tests for `GitRepositoryVirtualRefsSyncJob`."""
496
497 layer = ZopelessDatabaseLayer
498
499 def runJobs(self):
500 with dbuser("branchscanner"):
501 job_set = JobRunner.fromReady(
502 getUtility(IGitRepositoryVirtualRefsSyncJobSource))
503 job_set.runAll()
504 return job_set
505
506 def test_changing_repo_to_private_deletes_refs(self):
507 self.useFixture(FeatureFixture({GIT_CREATE_MP_VIRTUAL_REF: "on"}))
508 hosting_fixture = self.useFixture(GitHostingFixture())
509 mp = self.factory.makeBranchMergeProposalForGit()
510 source_repo = mp.source_git_repository
511 target_repo = mp.target_git_repository
512 source_repo.transitionToInformationType(
513 InformationType.PRIVATESECURITY, source_repo.owner, False)
514
515 hosting_fixture.copyRefs.resetCalls()
516 hosting_fixture.deleteRef.resetCalls()
517 jobs = self.runJobs()
518 self.assertEqual(1, len(jobs.completed_jobs))
519
520 self.assertEqual(0, hosting_fixture.copyRefs.call_count)
521 self.assertEqual(1, hosting_fixture.deleteRef.call_count)
522 args, kwargs = hosting_fixture.deleteRef.calls[0]
523 self.assertEqual(
524 (target_repo.getInternalPath(), 'refs/merge/%s/head' % mp.id),
525 args)
526
527 def test_changing_repo_to_public_recreates_refs(self):
528 self.useFixture(FeatureFixture({GIT_CREATE_MP_VIRTUAL_REF: "on"}))
529 hosting_fixture = self.useFixture(GitHostingFixture())
530 mp = self.factory.makeBranchMergeProposalForGit()
531 source_repo = mp.source_git_repository
532 source_repo.transitionToInformationType(
533 InformationType.PRIVATESECURITY, source_repo.owner, False)
534 self.runJobs()
535
536 # Move it back to public.
537 hosting_fixture.copyRefs.resetCalls()
538 hosting_fixture.deleteRef.resetCalls()
539 source_repo.transitionToInformationType(
540 InformationType.PUBLIC, source_repo.owner, False)
541 jobs = self.runJobs()
542 self.assertEqual(1, len(jobs.completed_jobs))
543
544 self.assertEqual(1, hosting_fixture.copyRefs.call_count)
545 self.assertEqual(0, hosting_fixture.deleteRef.call_count)
546 args, kwargs = hosting_fixture.copyRefs.calls[0]
547 self.assertEqual({'logger': mock.ANY}, kwargs)
548 self.assertEqual(2, len(args))
549 repo, operations = args
550 self.assertEqual(repo, mp.source_git_repository.getInternalPath())
551 self.assertThat(operations, MatchesListwise([
552 MatchesStructure(
553 source_ref=Equals(mp.source_git_commit_sha1),
554 target_repo=Equals(mp.target_git_repository.getInternalPath()),
555 target_ref=Equals("refs/merge/%s/head" % mp.id),
556 )
557 ]))
558
559 @mock.patch("lp.code.model.branchmergeproposal.BranchMergeProposal."
560 "syncGitHostingVirtualRefs")
561 def test_changing_repo_retry_skips_successful_syncs(
562 self, syncGitHostingVirtualRefs):
563 # Makes sure that successful syncs are not retried on failures.
564 self.useFixture(FeatureFixture({GIT_CREATE_MP_VIRTUAL_REF: "on"}))
565 hosting_fixture = self.useFixture(GitHostingFixture())
566 mp1 = self.factory.makeBranchMergeProposalForGit()
567 source_repo = mp1.source_git_repository
568
569 mp2 = self.factory.makeBranchMergeProposalForGit(
570 source_ref=mp1.source_git_ref)
571 mp3 = self.factory.makeBranchMergeProposalForGit(
572 source_ref=mp1.source_git_ref)
573
574 # The 1st call to syncGitHostingVirtualRefs (mp1) should succeed.
575 # The 2nd call (mp2, first try) should fail
576 # The 3rd call (mp3) should succeed.
577 # Then, 4th call (mp2 again) should succeed.
578 syncGitHostingVirtualRefs.reset_mock()
579 syncGitHostingVirtualRefs.side_effect = [None, Exception(), None, None]
580
581 # Make source repo private, to trigger the job.
582 hosting_fixture.copyRefs.resetCalls()
583 hosting_fixture.deleteRef.resetCalls()
584 source_repo.transitionToInformationType(
585 InformationType.PRIVATESECURITY, source_repo.owner, False)
586 jobs = self.runJobs()
587 # Should have no completed jobs, since the job should be retried.
588 self.assertEqual(
589 0, len(jobs.completed_jobs),
590 "No job should have been finished.")
591 self.assertEqual(
592 1, len(jobs.incomplete_jobs), "Job retry should be pending.")
593 self.assertEqual(
594 3, syncGitHostingVirtualRefs.call_count,
595 "Even with a failure on mp2, syncGitHostingVirtualRefs should "
596 "have been called for every branch merge proposal.")
597 job = removeSecurityProxy(jobs.incomplete_jobs[0])
598 self.assertEqual([mp1.id, mp3.id], job.synced_mp_ids)
599
600 # Run the job again.
601 syncGitHostingVirtualRefs.reset_mock()
602 JobRunner(jobs.incomplete_jobs).runAll()
603 self.assertEqual(
604 JobStatus.COMPLETED, jobs.incomplete_jobs[0].status,
605 "The job should have completed this time, since mp2 sync should "
606 "not have raised exception.")
607 self.assertEqual(
608 1, syncGitHostingVirtualRefs.call_count,
609 "mp2.syncGitHostingVirtualRefs should be the only call to this "
610 "method, since the sync should have been skipped for mp1 and mp3.")
611 self.assertEqual([mp1.id, mp3.id, mp2.id], job.synced_mp_ids)
612
487# XXX cjwatson 2015-03-12: We should test that the jobs work via Celery too,613# XXX cjwatson 2015-03-12: We should test that the jobs work via Celery too,
488# but that isn't feasible until we have a proper turnip fixture.614# but that isn't feasible until we have a proper turnip fixture.
diff --git a/lib/lp/code/model/tests/test_gitrepository.py b/lib/lp/code/model/tests/test_gitrepository.py
index b0116e1..c561b3c 100644
--- a/lib/lp/code/model/tests/test_gitrepository.py
+++ b/lib/lp/code/model/tests/test_gitrepository.py
@@ -18,6 +18,7 @@ from datetime import (
18import email18import email
19from functools import partial19from functools import partial
20import hashlib20import hashlib
21import itertools
21import json22import json
2223
23from breezy import urlutils24from breezy import urlutils
@@ -89,6 +90,7 @@ from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository
89from lp.code.interfaces.gitjob import (90from lp.code.interfaces.gitjob import (
90 IGitRefScanJobSource,91 IGitRefScanJobSource,
91 IGitRepositoryModifiedMailJobSource,92 IGitRepositoryModifiedMailJobSource,
93 IGitRepositoryVirtualRefsSyncJobSource,
92 )94 )
93from lp.code.interfaces.gitlookup import IGitLookup95from lp.code.interfaces.gitlookup import IGitLookup
94from lp.code.interfaces.gitnamespace import (96from lp.code.interfaces.gitnamespace import (
@@ -147,6 +149,7 @@ from lp.registry.interfaces.personociproject import IPersonOCIProjectFactory
147from lp.registry.interfaces.personproduct import IPersonProductFactory149from lp.registry.interfaces.personproduct import IPersonProductFactory
148from lp.registry.tests.test_accesspolicy import get_policies_for_artifact150from lp.registry.tests.test_accesspolicy import get_policies_for_artifact
149from lp.services.authserver.xmlrpc import AuthServerAPIView151from lp.services.authserver.xmlrpc import AuthServerAPIView
152from lp.services.compat import mock
150from lp.services.config import config153from lp.services.config import config
151from lp.services.database.constants import UTC_NOW154from lp.services.database.constants import UTC_NOW
152from lp.services.database.interfaces import IStore155from lp.services.database.interfaces import IStore
@@ -181,6 +184,7 @@ from lp.testing import (
181 verifyObject,184 verifyObject,
182 )185 )
183from lp.testing.dbuser import dbuser186from lp.testing.dbuser import dbuser
187from lp.testing.fixture import ZopeUtilityFixture
184from lp.testing.layers import (188from lp.testing.layers import (
185 DatabaseFunctionalLayer,189 DatabaseFunctionalLayer,
186 LaunchpadFunctionalLayer,190 LaunchpadFunctionalLayer,
@@ -4662,3 +4666,61 @@ class TestGitRepositoryMacaroonIssuer(MacaroonTestMixin, TestCaseWithFactory):
4662 ["Caveat check for '%s' failed." %4666 ["Caveat check for '%s' failed." %
4663 find_caveats_by_name(macaroon2, "lp.expires")[0].caveat_id],4667 find_caveats_by_name(macaroon2, "lp.expires")[0].caveat_id],
4664 issuer, macaroon2, repository, user=repository.owner)4668 issuer, macaroon2, repository, user=repository.owner)
4669
4670
4671class TestGitRepositoryPrivacyChangeSyncVirtualRefs(TestCaseWithFactory):
4672 layer = DatabaseFunctionalLayer
4673
4674 def assertChangePrivacyTriggersSync(
4675 self, from_list, to_list, should_trigger_sync=True):
4676 """Runs repository.transitionToInformationType from every item in
4677 `from_list` to each item in `to_list`, and checks if the virtual
4678 refs sync was triggered or not, depending on `should_trigger_sync`."""
4679 sync_job = mock.Mock()
4680 self.useFixture(ZopeUtilityFixture(
4681 sync_job, IGitRepositoryVirtualRefsSyncJobSource))
4682
4683 admin = self.factory.makeAdministrator()
4684 login_person(admin)
4685 for from_type, to_type in itertools.product(from_list, to_list):
4686 if from_type == to_type:
4687 continue
4688 repository = self.factory.makeGitRepository()
4689 naked_repo = removeSecurityProxy(repository)
4690 naked_repo.information_type = from_type
4691 # Skip access policy reconciliation.
4692 naked_repo._reconcileAccess = mock.Mock()
4693 naked_repo.transitionToInformationType(to_type, admin, False)
4694
4695 if should_trigger_sync:
4696 sync_job.create.assert_called_with(repository)
4697 else:
4698 self.assertEqual(
4699 0, sync_job.create.call_count,
4700 "Changing from %s to %s should't trigger vrefs sync"
4701 % (from_type, to_type))
4702 sync_job.reset_mock()
4703
4704 def test_setting_repo_public_triggers_ref_sync_job(self):
4705 self.assertChangePrivacyTriggersSync(
4706 PRIVATE_INFORMATION_TYPES,
4707 PUBLIC_INFORMATION_TYPES,
4708 should_trigger_sync=True)
4709
4710 def test_setting_repo_private_triggers_ref_sync_job(self):
4711 self.assertChangePrivacyTriggersSync(
4712 PUBLIC_INFORMATION_TYPES,
4713 PRIVATE_INFORMATION_TYPES,
4714 should_trigger_sync=True)
4715
4716 def test_keeping_repo_private_dont_trigger_ref_sync_job(self):
4717 self.assertChangePrivacyTriggersSync(
4718 PRIVATE_INFORMATION_TYPES,
4719 PRIVATE_INFORMATION_TYPES,
4720 should_trigger_sync=False)
4721
4722 def test_keeping_repo_public_dont_trigger_ref_sync_job(self):
4723 self.assertChangePrivacyTriggersSync(
4724 PUBLIC_INFORMATION_TYPES,
4725 PUBLIC_INFORMATION_TYPES,
4726 should_trigger_sync=False)
diff --git a/lib/lp/services/config/schema-lazr.conf b/lib/lp/services/config/schema-lazr.conf
index bb8afcd..3921da2 100644
--- a/lib/lp/services/config/schema-lazr.conf
+++ b/lib/lp/services/config/schema-lazr.conf
@@ -1828,6 +1828,10 @@ module: lp.code.interfaces.gitjob
1828dbuser: send-branch-mail1828dbuser: send-branch-mail
1829crontab_group: MAIN1829crontab_group: MAIN
18301830
1831[IGitRepositoryVirtualRefsSyncJobSource]
1832module: lp.code.interfaces.gitjob
1833dbuser: branchscanner
1834
1831[IInitializeDistroSeriesJobSource]1835[IInitializeDistroSeriesJobSource]
1832module: lp.soyuz.interfaces.distributionjob1836module: lp.soyuz.interfaces.distributionjob
1833dbuser: initializedistroseries1837dbuser: initializedistroseries
diff --git a/lib/lp/testing/fakemethod.py b/lib/lp/testing/fakemethod.py
index 4bba8d3..b4895ce 100644
--- a/lib/lp/testing/fakemethod.py
+++ b/lib/lp/testing/fakemethod.py
@@ -1,4 +1,4 @@
1# Copyright 2009-2011 Canonical Ltd. This software is licensed under the1# Copyright 2009-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__metaclass__ = type4__metaclass__ = type
@@ -57,3 +57,6 @@ class FakeMethod:
57 def extract_kwargs(self):57 def extract_kwargs(self):
58 """Return just the calls' keyword-arguments dicts."""58 """Return just the calls' keyword-arguments dicts."""
59 return [kwargs for args, kwargs in self.calls]59 return [kwargs for args, kwargs in self.calls]
60
61 def resetCalls(self):
62 self.calls = []