Merge ~pappacena/launchpad:mp-refs-privacy-change into launchpad:master
- Git
- lp:~pappacena/launchpad
- mp-refs-privacy-change
- Merge into master
Proposed by
Thiago F. Pappacena
Status: | Superseded |
---|---|
Proposed branch: | ~pappacena/launchpad:mp-refs-privacy-change |
Merge into: | launchpad:master |
Diff against target: |
1280 lines (+639/-33) 17 files modified
lib/lp/code/configure.zcml (+9/-0) lib/lp/code/errors.py (+6/-1) lib/lp/code/interfaces/githosting.py (+19/-1) lib/lp/code/interfaces/gitjob.py (+16/-1) lib/lp/code/interfaces/gitrepository.py (+12/-1) lib/lp/code/model/branchmergeproposal.py (+75/-0) lib/lp/code/model/githosting.py (+63/-2) lib/lp/code/model/gitjob.py (+42/-1) lib/lp/code/model/gitrepository.py (+18/-2) lib/lp/code/model/tests/test_branchmergeproposal.py (+154/-0) lib/lp/code/model/tests/test_githosting.py (+45/-1) lib/lp/code/model/tests/test_gitjob.py (+36/-1) lib/lp/code/model/tests/test_gitrepository.py (+128/-19) lib/lp/code/subscribers/branchmergeproposal.py (+6/-2) lib/lp/code/tests/helpers.py (+2/-0) lib/lp/services/config/schema-lazr.conf (+4/-0) lib/lp/testing/fakemethod.py (+4/-1) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Launchpad code reviewers | Pending | ||
Review via email: mp+390939@code.launchpad.net |
Commit message
Background job to add/remove merge proposal's virtual refs when a repository has its privacy changed
Description of the change
To post a comment you must log in.
Unmerged commits
- 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 - a803723... by Thiago F. Pappacena
-
Fixing interface method definition
- 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
- 50eeb2e... by Thiago F. Pappacena
-
Adding feature flag to control mp virtual refs creation
- 84d5c5f... by Thiago F. Pappacena
-
Logging error
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/lib/lp/code/configure.zcml b/lib/lp/code/configure.zcml | |||
2 | index 898e645..4adc8d0 100644 | |||
3 | --- a/lib/lp/code/configure.zcml | |||
4 | +++ b/lib/lp/code/configure.zcml | |||
5 | @@ -1111,6 +1111,11 @@ | |||
6 | 1111 | provides="lp.code.interfaces.gitjob.IGitRepositoryModifiedMailJobSource"> | 1111 | provides="lp.code.interfaces.gitjob.IGitRepositoryModifiedMailJobSource"> |
7 | 1112 | <allow interface="lp.code.interfaces.gitjob.IGitRepositoryModifiedMailJobSource" /> | 1112 | <allow interface="lp.code.interfaces.gitjob.IGitRepositoryModifiedMailJobSource" /> |
8 | 1113 | </securedutility> | 1113 | </securedutility> |
9 | 1114 | <securedutility | ||
10 | 1115 | component="lp.code.model.gitjob.GitRepositoryVirtualRefsSyncJob" | ||
11 | 1116 | provides="lp.code.interfaces.gitjob.IGitRepositoryVirtualRefsSyncJobSource"> | ||
12 | 1117 | <allow interface="lp.code.interfaces.gitjob.IGitRepositoryVirtualRefsSyncJobSource" /> | ||
13 | 1118 | </securedutility> | ||
14 | 1114 | <class class="lp.code.model.gitjob.GitRefScanJob"> | 1119 | <class class="lp.code.model.gitjob.GitRefScanJob"> |
15 | 1115 | <allow interface="lp.code.interfaces.gitjob.IGitJob" /> | 1120 | <allow interface="lp.code.interfaces.gitjob.IGitJob" /> |
16 | 1116 | <allow interface="lp.code.interfaces.gitjob.IGitRefScanJob" /> | 1121 | <allow interface="lp.code.interfaces.gitjob.IGitRefScanJob" /> |
17 | @@ -1123,6 +1128,10 @@ | |||
18 | 1123 | <allow interface="lp.code.interfaces.gitjob.IGitJob" /> | 1128 | <allow interface="lp.code.interfaces.gitjob.IGitJob" /> |
19 | 1124 | <allow interface="lp.code.interfaces.gitjob.IGitRepositoryModifiedMailJob" /> | 1129 | <allow interface="lp.code.interfaces.gitjob.IGitRepositoryModifiedMailJob" /> |
20 | 1125 | </class> | 1130 | </class> |
21 | 1131 | <class class="lp.code.model.gitjob.GitRepositoryVirtualRefsSyncJob"> | ||
22 | 1132 | <allow interface="lp.code.interfaces.gitjob.IGitJob" /> | ||
23 | 1133 | <allow interface="lp.code.interfaces.gitjob.IGitRepositoryVirtualRefsSyncJob" /> | ||
24 | 1134 | </class> | ||
25 | 1126 | 1135 | ||
26 | 1127 | <lp:help-folder folder="help" name="+help-code" /> | 1136 | <lp:help-folder folder="help" name="+help-code" /> |
27 | 1128 | 1137 | ||
28 | diff --git a/lib/lp/code/errors.py b/lib/lp/code/errors.py | |||
29 | index 85de140..bed7db5 100644 | |||
30 | --- a/lib/lp/code/errors.py | |||
31 | +++ b/lib/lp/code/errors.py | |||
32 | @@ -1,4 +1,4 @@ | |||
34 | 1 | # Copyright 2009-2019 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2020 Canonical Ltd. This software is licensed under the |
35 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
36 | 3 | 3 | ||
37 | 4 | """Errors used in the lp/code modules.""" | 4 | """Errors used in the lp/code modules.""" |
38 | @@ -34,6 +34,7 @@ __all__ = [ | |||
39 | 34 | 'ClaimReviewFailed', | 34 | 'ClaimReviewFailed', |
40 | 35 | 'DiffNotFound', | 35 | 'DiffNotFound', |
41 | 36 | 'GitDefaultConflict', | 36 | 'GitDefaultConflict', |
42 | 37 | 'GitReferenceDeletionFault', | ||
43 | 37 | 'GitRepositoryBlobNotFound', | 38 | 'GitRepositoryBlobNotFound', |
44 | 38 | 'GitRepositoryBlobUnsupportedRemote', | 39 | 'GitRepositoryBlobUnsupportedRemote', |
45 | 39 | 'GitRepositoryCreationException', | 40 | 'GitRepositoryCreationException', |
46 | @@ -493,6 +494,10 @@ class GitRepositoryDeletionFault(Exception): | |||
47 | 493 | """Raised when there is a fault deleting a repository.""" | 494 | """Raised when there is a fault deleting a repository.""" |
48 | 494 | 495 | ||
49 | 495 | 496 | ||
50 | 497 | class GitReferenceDeletionFault(Exception): | ||
51 | 498 | """Raised when there is a fault deleting a repository's ref.""" | ||
52 | 499 | |||
53 | 500 | |||
54 | 496 | class GitTargetError(Exception): | 501 | class GitTargetError(Exception): |
55 | 497 | """Raised when there is an error determining a Git repository target.""" | 502 | """Raised when there is an error determining a Git repository target.""" |
56 | 498 | 503 | ||
57 | diff --git a/lib/lp/code/interfaces/githosting.py b/lib/lp/code/interfaces/githosting.py | |||
58 | index 378930a..d7b048f 100644 | |||
59 | --- a/lib/lp/code/interfaces/githosting.py | |||
60 | +++ b/lib/lp/code/interfaces/githosting.py | |||
61 | @@ -1,4 +1,4 @@ | |||
63 | 1 | # Copyright 2015-2018 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2015-2020 Canonical Ltd. This software is licensed under the |
64 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
65 | 3 | 3 | ||
66 | 4 | """Interface for communication with the Git hosting service.""" | 4 | """Interface for communication with the Git hosting service.""" |
67 | @@ -129,3 +129,21 @@ class IGitHostingClient(Interface): | |||
68 | 129 | :param logger: An optional logger. | 129 | :param logger: An optional logger. |
69 | 130 | :return: A binary string with the blob content. | 130 | :return: A binary string with the blob content. |
70 | 131 | """ | 131 | """ |
71 | 132 | |||
72 | 133 | def copyRefs(path, operations, logger=None): | ||
73 | 134 | """Executes the copy of refs or commits between different | ||
74 | 135 | repositories. | ||
75 | 136 | |||
76 | 137 | :param path: Physical path of the repository on the hosting service. | ||
77 | 138 | :param operations: A list of RefCopyOperation objects describing | ||
78 | 139 | source and target of the copy. | ||
79 | 140 | :param logger: An optional logger. | ||
80 | 141 | """ | ||
81 | 142 | |||
82 | 143 | def deleteRef(path, ref, logger=None): | ||
83 | 144 | """Deletes a reference on the given git repository. | ||
84 | 145 | |||
85 | 146 | :param path: Physical path of the repository on the hosting service. | ||
86 | 147 | :param ref: The reference to be delete. | ||
87 | 148 | :param logger: An optional logger. | ||
88 | 149 | """ | ||
89 | diff --git a/lib/lp/code/interfaces/gitjob.py b/lib/lp/code/interfaces/gitjob.py | |||
90 | index 4f31b19..7c3c038 100644 | |||
91 | --- a/lib/lp/code/interfaces/gitjob.py | |||
92 | +++ b/lib/lp/code/interfaces/gitjob.py | |||
93 | @@ -1,4 +1,4 @@ | |||
95 | 1 | # Copyright 2015 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2015-2020 Canonical Ltd. This software is licensed under the |
96 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
97 | 3 | 3 | ||
98 | 4 | """GitJob interfaces.""" | 4 | """GitJob interfaces.""" |
99 | @@ -11,6 +11,8 @@ __all__ = [ | |||
100 | 11 | 'IGitRefScanJobSource', | 11 | 'IGitRefScanJobSource', |
101 | 12 | 'IGitRepositoryModifiedMailJob', | 12 | 'IGitRepositoryModifiedMailJob', |
102 | 13 | 'IGitRepositoryModifiedMailJobSource', | 13 | 'IGitRepositoryModifiedMailJobSource', |
103 | 14 | 'IGitRepositoryVirtualRefsSyncJob', | ||
104 | 15 | 'IGitRepositoryVirtualRefsSyncJobSource', | ||
105 | 14 | 'IReclaimGitRepositorySpaceJob', | 16 | 'IReclaimGitRepositorySpaceJob', |
106 | 15 | 'IReclaimGitRepositorySpaceJobSource', | 17 | 'IReclaimGitRepositorySpaceJobSource', |
107 | 16 | ] | 18 | ] |
108 | @@ -93,3 +95,16 @@ class IGitRepositoryModifiedMailJobSource(IJobSource): | |||
109 | 93 | :param repository_delta: An `IGitRepositoryDelta` describing the | 95 | :param repository_delta: An `IGitRepositoryDelta` describing the |
110 | 94 | changes. | 96 | changes. |
111 | 95 | """ | 97 | """ |
112 | 98 | |||
113 | 99 | |||
114 | 100 | class IGitRepositoryVirtualRefsSyncJob(IRunnableJob): | ||
115 | 101 | """A job to synchronize all MPs virtual refs related to this repository.""" | ||
116 | 102 | |||
117 | 103 | |||
118 | 104 | class IGitRepositoryVirtualRefsSyncJobSource(IJobSource): | ||
119 | 105 | |||
120 | 106 | def create(repository): | ||
121 | 107 | """Send email about repository modifications. | ||
122 | 108 | |||
123 | 109 | :param repository: The `IGitRepository` that needs sync. | ||
124 | 110 | """ | ||
125 | diff --git a/lib/lp/code/interfaces/gitrepository.py b/lib/lp/code/interfaces/gitrepository.py | |||
126 | index 344bc43..9e4c129 100644 | |||
127 | --- a/lib/lp/code/interfaces/gitrepository.py | |||
128 | +++ b/lib/lp/code/interfaces/gitrepository.py | |||
129 | @@ -8,6 +8,8 @@ __metaclass__ = type | |||
130 | 8 | __all__ = [ | 8 | __all__ = [ |
131 | 9 | 'ContributorGitIdentity', | 9 | 'ContributorGitIdentity', |
132 | 10 | 'GitIdentityMixin', | 10 | 'GitIdentityMixin', |
133 | 11 | 'GIT_CREATE_MP_VIRTUAL_REF', | ||
134 | 12 | 'GIT_MP_VIRTUAL_REF_FORMAT', | ||
135 | 11 | 'GIT_REPOSITORY_NAME_VALIDATION_ERROR_MESSAGE', | 13 | 'GIT_REPOSITORY_NAME_VALIDATION_ERROR_MESSAGE', |
136 | 12 | 'git_repository_name_validator', | 14 | 'git_repository_name_validator', |
137 | 13 | 'IGitRepository', | 15 | 'IGitRepository', |
138 | @@ -105,6 +107,11 @@ valid_git_repository_name_pattern = re.compile( | |||
139 | 105 | r"^(?i)[a-z0-9][a-z0-9+\.\-@_]*\Z") | 107 | r"^(?i)[a-z0-9][a-z0-9+\.\-@_]*\Z") |
140 | 106 | 108 | ||
141 | 107 | 109 | ||
142 | 110 | # Virtual ref where we automatically put a copy of any open merge proposal. | ||
143 | 111 | GIT_MP_VIRTUAL_REF_FORMAT = 'refs/merge/{mp.id}/head' | ||
144 | 112 | GIT_CREATE_MP_VIRTUAL_REF = 'git.mergeproposal_virtualref.enabled' | ||
145 | 113 | |||
146 | 114 | |||
147 | 108 | def valid_git_repository_name(name): | 115 | def valid_git_repository_name(name): |
148 | 109 | """Return True iff the name is valid as a Git repository name. | 116 | """Return True iff the name is valid as a Git repository name. |
149 | 110 | 117 | ||
150 | @@ -595,11 +602,15 @@ class IGitRepositoryView(IHasRecipes): | |||
151 | 595 | def updateLandingTargets(paths): | 602 | def updateLandingTargets(paths): |
152 | 596 | """Update landing targets (MPs where this repository is the source). | 603 | """Update landing targets (MPs where this repository is the source). |
153 | 597 | 604 | ||
155 | 598 | For each merge proposal, create `UpdatePreviewDiffJob`s. | 605 | For each merge proposal, create `UpdatePreviewDiffJob`s, and runs |
156 | 606 | the appropriate GitHosting.copyRefs operation to allow having | ||
157 | 607 | virtual merge refs with the updated branch version. | ||
158 | 599 | 608 | ||
159 | 600 | :param paths: A list of reference paths. Any merge proposals whose | 609 | :param paths: A list of reference paths. Any merge proposals whose |
160 | 601 | source is this repository and one of these paths will have their | 610 | source is this repository and one of these paths will have their |
161 | 602 | diffs updated. | 611 | diffs updated. |
162 | 612 | :return: Returns a tuple with the list of background jobs created, | ||
163 | 613 | and the lits of refs copy requested to GitHosting. | ||
164 | 603 | """ | 614 | """ |
165 | 604 | 615 | ||
166 | 605 | def markRecipesStale(paths): | 616 | def markRecipesStale(paths): |
167 | diff --git a/lib/lp/code/model/branchmergeproposal.py b/lib/lp/code/model/branchmergeproposal.py | |||
168 | index 2a346b4..bd804a8 100644 | |||
169 | --- a/lib/lp/code/model/branchmergeproposal.py | |||
170 | +++ b/lib/lp/code/model/branchmergeproposal.py | |||
171 | @@ -12,6 +12,7 @@ __all__ = [ | |||
172 | 12 | 12 | ||
173 | 13 | from collections import defaultdict | 13 | from collections import defaultdict |
174 | 14 | from email.utils import make_msgid | 14 | from email.utils import make_msgid |
175 | 15 | import logging | ||
176 | 15 | from operator import attrgetter | 16 | from operator import attrgetter |
177 | 16 | import sys | 17 | import sys |
178 | 17 | 18 | ||
179 | @@ -83,7 +84,12 @@ from lp.code.interfaces.codereviewcomment import ICodeReviewComment | |||
180 | 83 | from lp.code.interfaces.codereviewinlinecomment import ( | 84 | from lp.code.interfaces.codereviewinlinecomment import ( |
181 | 84 | ICodeReviewInlineCommentSet, | 85 | ICodeReviewInlineCommentSet, |
182 | 85 | ) | 86 | ) |
183 | 87 | from lp.code.interfaces.githosting import IGitHostingClient | ||
184 | 86 | from lp.code.interfaces.gitref import IGitRef | 88 | from lp.code.interfaces.gitref import IGitRef |
185 | 89 | from lp.code.interfaces.gitrepository import ( | ||
186 | 90 | GIT_CREATE_MP_VIRTUAL_REF, | ||
187 | 91 | GIT_MP_VIRTUAL_REF_FORMAT, | ||
188 | 92 | ) | ||
189 | 87 | from lp.code.mail.branch import RecipientReason | 93 | from lp.code.mail.branch import RecipientReason |
190 | 88 | from lp.code.model.branchrevision import BranchRevision | 94 | from lp.code.model.branchrevision import BranchRevision |
191 | 89 | from lp.code.model.codereviewcomment import CodeReviewComment | 95 | from lp.code.model.codereviewcomment import CodeReviewComment |
192 | @@ -93,6 +99,7 @@ from lp.code.model.diff import ( | |||
193 | 93 | IncrementalDiff, | 99 | IncrementalDiff, |
194 | 94 | PreviewDiff, | 100 | PreviewDiff, |
195 | 95 | ) | 101 | ) |
196 | 102 | from lp.code.model.githosting import RefCopyOperation | ||
197 | 96 | from lp.registry.interfaces.person import ( | 103 | from lp.registry.interfaces.person import ( |
198 | 97 | IPerson, | 104 | IPerson, |
199 | 98 | IPersonSet, | 105 | IPersonSet, |
200 | @@ -122,6 +129,7 @@ from lp.services.database.sqlbase import ( | |||
201 | 122 | quote, | 129 | quote, |
202 | 123 | SQLBase, | 130 | SQLBase, |
203 | 124 | ) | 131 | ) |
204 | 132 | from lp.services.features import getFeatureFlag | ||
205 | 125 | from lp.services.helpers import shortlist | 133 | from lp.services.helpers import shortlist |
206 | 126 | from lp.services.job.interfaces.job import JobStatus | 134 | from lp.services.job.interfaces.job import JobStatus |
207 | 127 | from lp.services.job.model.job import Job | 135 | from lp.services.job.model.job import Job |
208 | @@ -143,6 +151,9 @@ from lp.soyuz.enums import ( | |||
209 | 143 | ) | 151 | ) |
210 | 144 | 152 | ||
211 | 145 | 153 | ||
212 | 154 | logger = logging.getLogger(__name__) | ||
213 | 155 | |||
214 | 156 | |||
215 | 146 | def is_valid_transition(proposal, from_state, next_state, user=None): | 157 | def is_valid_transition(proposal, from_state, next_state, user=None): |
216 | 147 | """Is it valid for this user to move this proposal to to next_state? | 158 | """Is it valid for this user to move this proposal to to next_state? |
217 | 148 | 159 | ||
218 | @@ -968,6 +979,8 @@ class BranchMergeProposal(SQLBase, BugLinkTargetMixin): | |||
219 | 968 | branch_merge_proposal=self.id): | 979 | branch_merge_proposal=self.id): |
220 | 969 | job.destroySelf() | 980 | job.destroySelf() |
221 | 970 | self._preview_diffs.remove() | 981 | self._preview_diffs.remove() |
222 | 982 | self.deleteGitHostingVirtualRefs() | ||
223 | 983 | |||
224 | 971 | self.destroySelf() | 984 | self.destroySelf() |
225 | 972 | 985 | ||
226 | 973 | def getUnlandedSourceBranchRevisions(self): | 986 | def getUnlandedSourceBranchRevisions(self): |
227 | @@ -1214,6 +1227,68 @@ class BranchMergeProposal(SQLBase, BugLinkTargetMixin): | |||
228 | 1214 | for diff in diffs) | 1227 | for diff in diffs) |
229 | 1215 | return [diff_dict.get(revisions) for revisions in revision_list] | 1228 | return [diff_dict.get(revisions) for revisions in revision_list] |
230 | 1216 | 1229 | ||
231 | 1230 | def getGitHostingRefCopyOperations(self): | ||
232 | 1231 | """Gets the list of GitHosting copy operations that should be done | ||
233 | 1232 | in order to keep this MP's virtual refs up-to-date.""" | ||
234 | 1233 | if (not self.target_git_repository | ||
235 | 1234 | or not getFeatureFlag(GIT_CREATE_MP_VIRTUAL_REF)): | ||
236 | 1235 | return [] | ||
237 | 1236 | # If the source repository is private and it's opening a MP to | ||
238 | 1237 | # another repository, we should not risk to to have the private code | ||
239 | 1238 | # undisclosed to everyone with permission to see the target repo: | ||
240 | 1239 | private_source = self.source_git_repository.private | ||
241 | 1240 | same_repo = self.source_git_repository == self.target_git_repository | ||
242 | 1241 | same_owner = self.source_git_repository.owner.inTeam( | ||
243 | 1242 | self.target_git_repository.owner) | ||
244 | 1243 | if private_source and not (same_repo or same_owner): | ||
245 | 1244 | return [] | ||
246 | 1245 | return [RefCopyOperation( | ||
247 | 1246 | self.source_git_commit_sha1, | ||
248 | 1247 | self.target_git_repository.getInternalPath(), | ||
249 | 1248 | GIT_MP_VIRTUAL_REF_FORMAT.format(mp=self))] | ||
250 | 1249 | |||
251 | 1250 | def copyGitHostingVirtualRefs(self, logger=logger): | ||
252 | 1251 | """Requests virtual refs copy operations on GitHosting in order to | ||
253 | 1252 | keep them up-to-date with current MP's state. | ||
254 | 1253 | |||
255 | 1254 | :return: The list of copy operations requested.""" | ||
256 | 1255 | copy_operations = self.getGitHostingRefCopyOperations() | ||
257 | 1256 | if copy_operations: | ||
258 | 1257 | hosting_client = getUtility(IGitHostingClient) | ||
259 | 1258 | hosting_client.copyRefs( | ||
260 | 1259 | self.source_git_repository.getInternalPath(), | ||
261 | 1260 | copy_operations, logger=logger) | ||
262 | 1261 | return copy_operations | ||
263 | 1262 | |||
264 | 1263 | def deleteGitHostingVirtualRefs(self, except_refs=None, logger=None): | ||
265 | 1264 | """Deletes on git code hosting service all virtual refs, except | ||
266 | 1265 | those ones in the given list.""" | ||
267 | 1266 | if self.source_git_ref is None: | ||
268 | 1267 | return | ||
269 | 1268 | if not getFeatureFlag(GIT_CREATE_MP_VIRTUAL_REF): | ||
270 | 1269 | # XXX pappacena 2020-09-15: Do not try to remove virtual refs if | ||
271 | 1270 | # the feature is disabled. It might be disabled due to bug on | ||
272 | 1271 | # the first versions, for example, and we should have a way to | ||
273 | 1272 | # skip this feature entirely. | ||
274 | 1273 | # Once the feature is stable, we should actually always delete | ||
275 | 1274 | # the virtual refs, since we don't know if the feature was | ||
276 | 1275 | # enabled or not when the branch was created. | ||
277 | 1276 | return | ||
278 | 1277 | ref = GIT_MP_VIRTUAL_REF_FORMAT.format(mp=self) | ||
279 | 1278 | if ref not in (except_refs or []): | ||
280 | 1279 | hosting_client = getUtility(IGitHostingClient) | ||
281 | 1280 | hosting_client.deleteRef( | ||
282 | 1281 | self.target_git_repository.getInternalPath(), ref, | ||
283 | 1282 | logger=logger) | ||
284 | 1283 | |||
285 | 1284 | def syncGitHostingVirtualRefs(self, logger=None): | ||
286 | 1285 | """Requests all copies and deletion of virtual refs to make git code | ||
287 | 1286 | hosting in sync with this MP.""" | ||
288 | 1287 | operations = self.copyGitHostingVirtualRefs(logger=logger) | ||
289 | 1288 | copied_refs = [i.target_ref for i in operations] | ||
290 | 1289 | self.deleteGitHostingVirtualRefs( | ||
291 | 1290 | except_refs=copied_refs, logger=logger) | ||
292 | 1291 | |||
293 | 1217 | def scheduleDiffUpdates(self, return_jobs=True): | 1292 | def scheduleDiffUpdates(self, return_jobs=True): |
294 | 1218 | """See `IBranchMergeProposal`.""" | 1293 | """See `IBranchMergeProposal`.""" |
295 | 1219 | from lp.code.model.branchmergeproposaljob import ( | 1294 | from lp.code.model.branchmergeproposaljob import ( |
296 | diff --git a/lib/lp/code/model/githosting.py b/lib/lp/code/model/githosting.py | |||
297 | index 94d6538..12a7d2f 100644 | |||
298 | --- a/lib/lp/code/model/githosting.py | |||
299 | +++ b/lib/lp/code/model/githosting.py | |||
300 | @@ -1,4 +1,4 @@ | |||
302 | 1 | # Copyright 2015-2019 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2015-2020 Canonical Ltd. This software is licensed under the |
303 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
304 | 3 | 3 | ||
305 | 4 | """Communication with the Git hosting service.""" | 4 | """Communication with the Git hosting service.""" |
306 | @@ -6,6 +6,7 @@ | |||
307 | 6 | __metaclass__ = type | 6 | __metaclass__ = type |
308 | 7 | __all__ = [ | 7 | __all__ = [ |
309 | 8 | 'GitHostingClient', | 8 | 'GitHostingClient', |
310 | 9 | 'RefCopyOperation', | ||
311 | 9 | ] | 10 | ] |
312 | 10 | 11 | ||
313 | 11 | import base64 | 12 | import base64 |
314 | @@ -14,7 +15,10 @@ import sys | |||
315 | 14 | 15 | ||
316 | 15 | from lazr.restful.utils import get_current_browser_request | 16 | from lazr.restful.utils import get_current_browser_request |
317 | 16 | import requests | 17 | import requests |
319 | 17 | from six import reraise | 18 | from six import ( |
320 | 19 | ensure_text, | ||
321 | 20 | reraise, | ||
322 | 21 | ) | ||
323 | 18 | from six.moves.urllib.parse import ( | 22 | from six.moves.urllib.parse import ( |
324 | 19 | quote, | 23 | quote, |
325 | 20 | urljoin, | 24 | urljoin, |
326 | @@ -22,10 +26,12 @@ from six.moves.urllib.parse import ( | |||
327 | 22 | from zope.interface import implementer | 26 | from zope.interface import implementer |
328 | 23 | 27 | ||
329 | 24 | from lp.code.errors import ( | 28 | from lp.code.errors import ( |
330 | 29 | GitReferenceDeletionFault, | ||
331 | 25 | GitRepositoryBlobNotFound, | 30 | GitRepositoryBlobNotFound, |
332 | 26 | GitRepositoryCreationFault, | 31 | GitRepositoryCreationFault, |
333 | 27 | GitRepositoryDeletionFault, | 32 | GitRepositoryDeletionFault, |
334 | 28 | GitRepositoryScanFault, | 33 | GitRepositoryScanFault, |
335 | 34 | GitTargetError, | ||
336 | 29 | ) | 35 | ) |
337 | 30 | from lp.code.interfaces.githosting import IGitHostingClient | 36 | from lp.code.interfaces.githosting import IGitHostingClient |
338 | 31 | from lp.services.config import config | 37 | from lp.services.config import config |
339 | @@ -41,6 +47,18 @@ class RequestExceptionWrapper(requests.RequestException): | |||
340 | 41 | """A non-requests exception that occurred during a request.""" | 47 | """A non-requests exception that occurred during a request.""" |
341 | 42 | 48 | ||
342 | 43 | 49 | ||
343 | 50 | class RefCopyOperation: | ||
344 | 51 | """A description of a ref (or commit) copy between repositories. | ||
345 | 52 | |||
346 | 53 | This class is just a helper to define copy operations parameters on | ||
347 | 54 | IGitHostingClient.copyRefs method. | ||
348 | 55 | """ | ||
349 | 56 | def __init__(self, source_ref, target_repo, target_ref): | ||
350 | 57 | self.source_ref = source_ref | ||
351 | 58 | self.target_repo = target_repo | ||
352 | 59 | self.target_ref = target_ref | ||
353 | 60 | |||
354 | 61 | |||
355 | 44 | @implementer(IGitHostingClient) | 62 | @implementer(IGitHostingClient) |
356 | 45 | class GitHostingClient: | 63 | class GitHostingClient: |
357 | 46 | """A client for the internal API provided by the Git hosting system.""" | 64 | """A client for the internal API provided by the Git hosting system.""" |
358 | @@ -237,3 +255,46 @@ class GitHostingClient: | |||
359 | 237 | except Exception as e: | 255 | except Exception as e: |
360 | 238 | raise GitRepositoryScanFault( | 256 | raise GitRepositoryScanFault( |
361 | 239 | "Failed to get file from Git repository: %s" % unicode(e)) | 257 | "Failed to get file from Git repository: %s" % unicode(e)) |
362 | 258 | |||
363 | 259 | def copyRefs(self, path, operations, logger=None): | ||
364 | 260 | """See `IGitHostingClient`.""" | ||
365 | 261 | json_data = { | ||
366 | 262 | "operations": [{ | ||
367 | 263 | "from": i.source_ref, | ||
368 | 264 | "to": {"repo": i.target_repo, "ref": i.target_ref} | ||
369 | 265 | } for i in operations] | ||
370 | 266 | } | ||
371 | 267 | try: | ||
372 | 268 | if logger is not None: | ||
373 | 269 | logger.info( | ||
374 | 270 | "Copying refs from %s to %s targets" % | ||
375 | 271 | (path, len(operations))) | ||
376 | 272 | url = "/repo/%s/refs-copy" % path | ||
377 | 273 | self._post(url, json=json_data) | ||
378 | 274 | except requests.RequestException as e: | ||
379 | 275 | if (e.response is not None and | ||
380 | 276 | e.response.status_code == requests.codes.NOT_FOUND): | ||
381 | 277 | raise GitTargetError( | ||
382 | 278 | "Could not find repository %s or one of its refs" % | ||
383 | 279 | ensure_text(path)) | ||
384 | 280 | else: | ||
385 | 281 | raise GitRepositoryScanFault( | ||
386 | 282 | "Could not copy refs: HTTP %s" % e.response.status_code) | ||
387 | 283 | |||
388 | 284 | def deleteRef(self, path, ref, logger=None): | ||
389 | 285 | try: | ||
390 | 286 | if logger is not None: | ||
391 | 287 | logger.info("Delete from repo %s the ref %s" % (path, ref)) | ||
392 | 288 | url = "/repo/%s/%s" % (path, ref) | ||
393 | 289 | self._delete(url) | ||
394 | 290 | except requests.RequestException as e: | ||
395 | 291 | if (e.response is not None and | ||
396 | 292 | e.response.status_code == requests.codes.NOT_FOUND): | ||
397 | 293 | if logger is not None: | ||
398 | 294 | logger.info( | ||
399 | 295 | "Tried to delete a ref that doesn't exist: %s (%s)" % | ||
400 | 296 | (path, ref)) | ||
401 | 297 | return | ||
402 | 298 | raise GitReferenceDeletionFault( | ||
403 | 299 | "Error deleting %s from repo %s: HTTP %s" % | ||
404 | 300 | (ref, path, e.response.status_code)) | ||
405 | diff --git a/lib/lp/code/model/gitjob.py b/lib/lp/code/model/gitjob.py | |||
406 | index 3c041da..a18d28e 100644 | |||
407 | --- a/lib/lp/code/model/gitjob.py | |||
408 | +++ b/lib/lp/code/model/gitjob.py | |||
409 | @@ -1,4 +1,4 @@ | |||
411 | 1 | # Copyright 2015-2018 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2015-2020 Canonical Ltd. This software is licensed under the |
412 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
413 | 3 | 3 | ||
414 | 4 | __metaclass__ = type | 4 | __metaclass__ = type |
415 | @@ -45,6 +45,8 @@ from lp.code.interfaces.gitjob import ( | |||
416 | 45 | IGitRefScanJobSource, | 45 | IGitRefScanJobSource, |
417 | 46 | IGitRepositoryModifiedMailJob, | 46 | IGitRepositoryModifiedMailJob, |
418 | 47 | IGitRepositoryModifiedMailJobSource, | 47 | IGitRepositoryModifiedMailJobSource, |
419 | 48 | IGitRepositoryVirtualRefsSyncJob, | ||
420 | 49 | IGitRepositoryVirtualRefsSyncJobSource, | ||
421 | 48 | IReclaimGitRepositorySpaceJob, | 50 | IReclaimGitRepositorySpaceJob, |
422 | 49 | IReclaimGitRepositorySpaceJobSource, | 51 | IReclaimGitRepositorySpaceJobSource, |
423 | 50 | ) | 52 | ) |
424 | @@ -100,6 +102,13 @@ class GitJobType(DBEnumeratedType): | |||
425 | 100 | modifications. | 102 | modifications. |
426 | 101 | """) | 103 | """) |
427 | 102 | 104 | ||
428 | 105 | SYNC_MP_VIRTUAL_REFS = DBItem(3, """ | ||
429 | 106 | Sync merge proposals virtual refs | ||
430 | 107 | |||
431 | 108 | This job runs against a repository to synchronize the virtual refs | ||
432 | 109 | from all merge proposals related to this repository. | ||
433 | 110 | """) | ||
434 | 111 | |||
435 | 103 | 112 | ||
436 | 104 | @implementer(IGitJob) | 113 | @implementer(IGitJob) |
437 | 105 | class GitJob(StormBase): | 114 | class GitJob(StormBase): |
438 | @@ -404,3 +413,35 @@ class GitRepositoryModifiedMailJob(GitJobDerived): | |||
439 | 404 | def run(self): | 413 | def run(self): |
440 | 405 | """See `IGitRepositoryModifiedMailJob`.""" | 414 | """See `IGitRepositoryModifiedMailJob`.""" |
441 | 406 | self.getMailer().sendAll() | 415 | self.getMailer().sendAll() |
442 | 416 | |||
443 | 417 | |||
444 | 418 | @implementer(IGitRepositoryVirtualRefsSyncJob) | ||
445 | 419 | @provider(IGitRepositoryVirtualRefsSyncJobSource) | ||
446 | 420 | class GitRepositoryVirtualRefsSyncJob(GitJobDerived): | ||
447 | 421 | """A Job that scans a Git repository for its current list of references.""" | ||
448 | 422 | class_job_type = GitJobType.SYNC_MP_VIRTUAL_REFS | ||
449 | 423 | |||
450 | 424 | max_retries = 5 | ||
451 | 425 | |||
452 | 426 | retry_error_types = tuple() | ||
453 | 427 | |||
454 | 428 | config = config.IGitRepositoryVirtualRefsSyncJobSource | ||
455 | 429 | |||
456 | 430 | @classmethod | ||
457 | 431 | def create(cls, repository): | ||
458 | 432 | metadata = {} | ||
459 | 433 | git_job = GitJob(repository, cls.class_job_type, metadata) | ||
460 | 434 | job = cls(git_job) | ||
461 | 435 | job.celeryRunOnCommit() | ||
462 | 436 | return job | ||
463 | 437 | |||
464 | 438 | def run(self): | ||
465 | 439 | log.info( | ||
466 | 440 | "Starting to re-sync virtual refs from repository %s", | ||
467 | 441 | self.repository) | ||
468 | 442 | for mp in self.repository.landing_targets: | ||
469 | 443 | log.info("Re-syncing virtual refs from MP %s", mp) | ||
470 | 444 | mp.syncGitHostingVirtualRefs(logger=log) | ||
471 | 445 | log.info( | ||
472 | 446 | "Finished re-syncing virtual refs from repository %s", | ||
473 | 447 | self.repository) | ||
474 | diff --git a/lib/lp/code/model/gitrepository.py b/lib/lp/code/model/gitrepository.py | |||
475 | index 77a4bbd..f9958c8 100644 | |||
476 | --- a/lib/lp/code/model/gitrepository.py | |||
477 | +++ b/lib/lp/code/model/gitrepository.py | |||
478 | @@ -116,7 +116,10 @@ from lp.code.interfaces.gitcollection import ( | |||
479 | 116 | IGitCollection, | 116 | IGitCollection, |
480 | 117 | ) | 117 | ) |
481 | 118 | from lp.code.interfaces.githosting import IGitHostingClient | 118 | from lp.code.interfaces.githosting import IGitHostingClient |
483 | 119 | from lp.code.interfaces.gitjob import IGitRefScanJobSource | 119 | from lp.code.interfaces.gitjob import ( |
484 | 120 | IGitRefScanJobSource, | ||
485 | 121 | IGitRepositoryVirtualRefsSyncJobSource, | ||
486 | 122 | ) | ||
487 | 120 | from lp.code.interfaces.gitlookup import IGitLookup | 123 | from lp.code.interfaces.gitlookup import IGitLookup |
488 | 121 | from lp.code.interfaces.gitnamespace import ( | 124 | from lp.code.interfaces.gitnamespace import ( |
489 | 122 | get_git_namespace, | 125 | get_git_namespace, |
490 | @@ -865,6 +868,7 @@ class GitRepository(StormBase, WebhookTargetMixin, GitIdentityMixin): | |||
491 | 865 | raise CannotChangeInformationType("Forbidden by project policy.") | 868 | raise CannotChangeInformationType("Forbidden by project policy.") |
492 | 866 | # XXX cjwatson 2019-03-29: Check privacy rules on snaps that use | 869 | # XXX cjwatson 2019-03-29: Check privacy rules on snaps that use |
493 | 867 | # this repository. | 870 | # this repository. |
494 | 871 | was_private = self.private | ||
495 | 868 | self.information_type = information_type | 872 | self.information_type = information_type |
496 | 869 | self._reconcileAccess() | 873 | self._reconcileAccess() |
497 | 870 | if (information_type in PRIVATE_INFORMATION_TYPES and | 874 | if (information_type in PRIVATE_INFORMATION_TYPES and |
498 | @@ -882,6 +886,12 @@ class GitRepository(StormBase, WebhookTargetMixin, GitIdentityMixin): | |||
499 | 882 | # subscriptions. | 886 | # subscriptions. |
500 | 883 | getUtility(IRemoveArtifactSubscriptionsJobSource).create(user, [self]) | 887 | getUtility(IRemoveArtifactSubscriptionsJobSource).create(user, [self]) |
501 | 884 | 888 | ||
502 | 889 | # If privacy changed, we need to re-sync all virtual refs from | ||
503 | 890 | # all MPs to avoid disclosing private code, or to add the virtual | ||
504 | 891 | # refs to the now public code. | ||
505 | 892 | if was_private != self.private: | ||
506 | 893 | getUtility(IGitRepositoryVirtualRefsSyncJobSource).create(self) | ||
507 | 894 | |||
508 | 885 | def setName(self, new_name, user): | 895 | def setName(self, new_name, user): |
509 | 886 | """See `IGitRepository`.""" | 896 | """See `IGitRepository`.""" |
510 | 887 | self.namespace.moveRepository(self, user, new_name=new_name) | 897 | self.namespace.moveRepository(self, user, new_name=new_name) |
511 | @@ -1160,9 +1170,15 @@ class GitRepository(StormBase, WebhookTargetMixin, GitIdentityMixin): | |||
512 | 1160 | def updateLandingTargets(self, paths): | 1170 | def updateLandingTargets(self, paths): |
513 | 1161 | """See `IGitRepository`.""" | 1171 | """See `IGitRepository`.""" |
514 | 1162 | jobs = [] | 1172 | jobs = [] |
515 | 1173 | copy_operations = [] | ||
516 | 1163 | for merge_proposal in self.getActiveLandingTargets(paths): | 1174 | for merge_proposal in self.getActiveLandingTargets(paths): |
517 | 1164 | jobs.extend(merge_proposal.scheduleDiffUpdates()) | 1175 | jobs.extend(merge_proposal.scheduleDiffUpdates()) |
519 | 1165 | return jobs | 1176 | copy_operations += merge_proposal.getGitHostingRefCopyOperations() |
520 | 1177 | |||
521 | 1178 | if copy_operations: | ||
522 | 1179 | hosting_client = getUtility(IGitHostingClient) | ||
523 | 1180 | hosting_client.copyRefs(self.getInternalPath(), copy_operations) | ||
524 | 1181 | return jobs, copy_operations | ||
525 | 1166 | 1182 | ||
526 | 1167 | def _getRecipes(self, paths=None): | 1183 | def _getRecipes(self, paths=None): |
527 | 1168 | """Undecorated version of recipes for use by `markRecipesStale`.""" | 1184 | """Undecorated version of recipes for use by `markRecipesStale`.""" |
528 | diff --git a/lib/lp/code/model/tests/test_branchmergeproposal.py b/lib/lp/code/model/tests/test_branchmergeproposal.py | |||
529 | index 0d2457d..277650c 100644 | |||
530 | --- a/lib/lp/code/model/tests/test_branchmergeproposal.py | |||
531 | +++ b/lib/lp/code/model/tests/test_branchmergeproposal.py | |||
532 | @@ -66,6 +66,10 @@ from lp.code.interfaces.branchmergeproposal import ( | |||
533 | 66 | IBranchMergeProposalGetter, | 66 | IBranchMergeProposalGetter, |
534 | 67 | IBranchMergeProposalJobSource, | 67 | IBranchMergeProposalJobSource, |
535 | 68 | ) | 68 | ) |
536 | 69 | from lp.code.interfaces.gitrepository import ( | ||
537 | 70 | GIT_CREATE_MP_VIRTUAL_REF, | ||
538 | 71 | GIT_MP_VIRTUAL_REF_FORMAT, | ||
539 | 72 | ) | ||
540 | 69 | from lp.code.model.branchmergeproposal import ( | 73 | from lp.code.model.branchmergeproposal import ( |
541 | 70 | BranchMergeProposal, | 74 | BranchMergeProposal, |
542 | 71 | BranchMergeProposalGetter, | 75 | BranchMergeProposalGetter, |
543 | @@ -96,6 +100,7 @@ from lp.testing import ( | |||
544 | 96 | ExpectedException, | 100 | ExpectedException, |
545 | 97 | launchpadlib_for, | 101 | launchpadlib_for, |
546 | 98 | login, | 102 | login, |
547 | 103 | login_admin, | ||
548 | 99 | login_person, | 104 | login_person, |
549 | 100 | person_logged_in, | 105 | person_logged_in, |
550 | 101 | TestCaseWithFactory, | 106 | TestCaseWithFactory, |
551 | @@ -255,6 +260,136 @@ class TestBranchMergeProposalPrivacy(TestCaseWithFactory): | |||
552 | 255 | self.assertContentEqual([owner, team], subscriptions) | 260 | self.assertContentEqual([owner, team], subscriptions) |
553 | 256 | 261 | ||
554 | 257 | 262 | ||
555 | 263 | class TestGitBranchMergeProposalVirtualRefs(TestCaseWithFactory): | ||
556 | 264 | """Ensure that BranchMergeProposal creation run the appropriate copy | ||
557 | 265 | and delete of virtual refs, like ref/merge/<id>/head.""" | ||
558 | 266 | |||
559 | 267 | layer = DatabaseFunctionalLayer | ||
560 | 268 | |||
561 | 269 | def setUp(self): | ||
562 | 270 | super(TestGitBranchMergeProposalVirtualRefs, self).setUp() | ||
563 | 271 | self.hosting_fixture = self.useFixture(GitHostingFixture()) | ||
564 | 272 | |||
565 | 273 | def test_copy_git_merge_virtual_ref(self): | ||
566 | 274 | self.useFixture(FeatureFixture({GIT_CREATE_MP_VIRTUAL_REF: "on"})) | ||
567 | 275 | mp = self.factory.makeBranchMergeProposalForGit() | ||
568 | 276 | |||
569 | 277 | copy_operations = mp.getGitHostingRefCopyOperations() | ||
570 | 278 | self.assertEqual(1, len(copy_operations)) | ||
571 | 279 | self.assertThat(copy_operations[0], MatchesStructure( | ||
572 | 280 | source_ref=Equals(mp.source_git_commit_sha1), | ||
573 | 281 | target_repo=Equals(mp.target_git_repository.getInternalPath()), | ||
574 | 282 | target_ref=Equals("refs/merge/%s/head" % mp.id), | ||
575 | 283 | )) | ||
576 | 284 | |||
577 | 285 | self.assertEqual(1, self.hosting_fixture.copyRefs.call_count) | ||
578 | 286 | args, kwargs = self.hosting_fixture.copyRefs.calls[0] | ||
579 | 287 | arg_path, arg_copy_operations = args | ||
580 | 288 | self.assertEqual({'logger': None}, kwargs) | ||
581 | 289 | self.assertEqual(mp.source_git_repository.getInternalPath(), arg_path) | ||
582 | 290 | self.assertEqual(1, len(arg_copy_operations)) | ||
583 | 291 | self.assertThat(arg_copy_operations[0], MatchesStructure( | ||
584 | 292 | source_ref=Equals(mp.source_git_commit_sha1), | ||
585 | 293 | target_repo=Equals(mp.target_git_repository.getInternalPath()), | ||
586 | 294 | target_ref=Equals("refs/merge/%s/head" % mp.id), | ||
587 | 295 | )) | ||
588 | 296 | |||
589 | 297 | def test_getGitHostingRefCopyOperations_private_source(self): | ||
590 | 298 | self.useFixture(FeatureFixture({GIT_CREATE_MP_VIRTUAL_REF: "on"})) | ||
591 | 299 | login_admin() | ||
592 | 300 | source_repo = self.factory.makeGitRepository( | ||
593 | 301 | information_type=InformationType.PRIVATESECURITY) | ||
594 | 302 | target_repo = self.factory.makeGitRepository(target=source_repo.target) | ||
595 | 303 | [source] = self.factory.makeGitRefs(source_repo) | ||
596 | 304 | [target] = self.factory.makeGitRefs(target_repo) | ||
597 | 305 | mp = self.factory.makeBranchMergeProposalForGit( | ||
598 | 306 | source_ref=source, target_ref=target) | ||
599 | 307 | self.assertEqual([], mp.getGitHostingRefCopyOperations()) | ||
600 | 308 | |||
601 | 309 | def test_getGitHostingRefCopyOperations_private_source_same_repo(self): | ||
602 | 310 | self.useFixture(FeatureFixture({GIT_CREATE_MP_VIRTUAL_REF: "on"})) | ||
603 | 311 | login_admin() | ||
604 | 312 | repo = self.factory.makeGitRepository( | ||
605 | 313 | information_type=InformationType.PRIVATESECURITY) | ||
606 | 314 | [source, target] = self.factory.makeGitRefs( | ||
607 | 315 | repo, ['refs/heads/bugfix', 'refs/heads/master']) | ||
608 | 316 | mp = self.factory.makeBranchMergeProposalForGit( | ||
609 | 317 | source_ref=source, target_ref=target) | ||
610 | 318 | operations = mp.getGitHostingRefCopyOperations() | ||
611 | 319 | self.assertEqual(1, len(operations)) | ||
612 | 320 | self.assertThat(operations[0], MatchesStructure( | ||
613 | 321 | source_ref=Equals(mp.source_git_commit_sha1), | ||
614 | 322 | target_repo=Equals(mp.target_git_repository.getInternalPath()), | ||
615 | 323 | target_ref=Equals(GIT_MP_VIRTUAL_REF_FORMAT.format(mp=mp)) | ||
616 | 324 | )) | ||
617 | 325 | |||
618 | 326 | def test_getGitHostingRefCopyOperations_private_source_same_owner(self): | ||
619 | 327 | self.useFixture(FeatureFixture({GIT_CREATE_MP_VIRTUAL_REF: "on"})) | ||
620 | 328 | login_admin() | ||
621 | 329 | source_repo = self.factory.makeGitRepository( | ||
622 | 330 | information_type=InformationType.PRIVATESECURITY) | ||
623 | 331 | target_repo = self.factory.makeGitRepository( | ||
624 | 332 | target=source_repo.target, owner=source_repo.owner) | ||
625 | 333 | [source] = self.factory.makeGitRefs(source_repo) | ||
626 | 334 | [target] = self.factory.makeGitRefs(target_repo) | ||
627 | 335 | mp = self.factory.makeBranchMergeProposalForGit( | ||
628 | 336 | source_ref=source, target_ref=target) | ||
629 | 337 | operations = mp.getGitHostingRefCopyOperations() | ||
630 | 338 | self.assertEqual(1, len(operations)) | ||
631 | 339 | self.assertThat(operations[0], MatchesStructure( | ||
632 | 340 | source_ref=Equals(mp.source_git_commit_sha1), | ||
633 | 341 | target_repo=Equals(mp.target_git_repository.getInternalPath()), | ||
634 | 342 | target_ref=Equals(GIT_MP_VIRTUAL_REF_FORMAT.format(mp=mp)) | ||
635 | 343 | )) | ||
636 | 344 | |||
637 | 345 | def test_syncGitHostingVirtualRefs(self): | ||
638 | 346 | self.useFixture(FeatureFixture({GIT_CREATE_MP_VIRTUAL_REF: "on"})) | ||
639 | 347 | login_admin() | ||
640 | 348 | login_admin() | ||
641 | 349 | source_repo = self.factory.makeGitRepository() | ||
642 | 350 | target_repo = self.factory.makeGitRepository(target=source_repo.target) | ||
643 | 351 | [source] = self.factory.makeGitRefs(source_repo) | ||
644 | 352 | [target] = self.factory.makeGitRefs(target_repo) | ||
645 | 353 | mp = self.factory.makeBranchMergeProposalForGit( | ||
646 | 354 | source_ref=source, target_ref=target) | ||
647 | 355 | |||
648 | 356 | # mp.syncGitHostingVirtualRefs should have been triggered by event. | ||
649 | 357 | # Check lp.code.subscribers.branchmergeproposal.merge_proposal_created. | ||
650 | 358 | self.assertEqual(1, self.hosting_fixture.copyRefs.call_count) | ||
651 | 359 | args, kwargs = self.hosting_fixture.copyRefs.calls[0] | ||
652 | 360 | self.assertEquals({'logger': None}, kwargs) | ||
653 | 361 | self.assertEqual(args[0], source_repo.getInternalPath()) | ||
654 | 362 | self.assertEqual(1, len(args[1])) | ||
655 | 363 | self.assertThat(args[1][0], MatchesStructure( | ||
656 | 364 | source_ref=Equals(mp.source_git_commit_sha1), | ||
657 | 365 | target_repo=Equals(mp.target_git_repository.getInternalPath()), | ||
658 | 366 | target_ref=Equals("refs/merge/%s/head" % mp.id), | ||
659 | 367 | )) | ||
660 | 368 | |||
661 | 369 | self.assertEqual(0, self.hosting_fixture.deleteRef.call_count) | ||
662 | 370 | |||
663 | 371 | def test_syncGitHostingVirtualRefs_private_source_deletes_ref(self): | ||
664 | 372 | self.useFixture(FeatureFixture({GIT_CREATE_MP_VIRTUAL_REF: "on"})) | ||
665 | 373 | login_admin() | ||
666 | 374 | source_repo = self.factory.makeGitRepository( | ||
667 | 375 | information_type=InformationType.PRIVATESECURITY) | ||
668 | 376 | target_repo = self.factory.makeGitRepository(target=source_repo.target) | ||
669 | 377 | [source] = self.factory.makeGitRefs(source_repo) | ||
670 | 378 | [target] = self.factory.makeGitRefs(target_repo) | ||
671 | 379 | mp = self.factory.makeBranchMergeProposalForGit( | ||
672 | 380 | source_ref=source, target_ref=target) | ||
673 | 381 | |||
674 | 382 | # mp.syncGitHostingVirtualRefs should have been triggered by event. | ||
675 | 383 | # Check lp.code.subscribers.branchmergeproposal.merge_proposal_created. | ||
676 | 384 | self.assertEqual(0, self.hosting_fixture.copyRefs.call_count) | ||
677 | 385 | self.assertEqual(1, self.hosting_fixture.deleteRef.call_count) | ||
678 | 386 | args, kwargs = self.hosting_fixture.deleteRef.calls[0] | ||
679 | 387 | self.assertEqual({'logger': None}, kwargs) | ||
680 | 388 | self.assertEqual( | ||
681 | 389 | (target_repo.getInternalPath(), "refs/merge/%s/head" % mp.id), | ||
682 | 390 | args) | ||
683 | 391 | |||
684 | 392 | |||
685 | 258 | class TestBranchMergeProposalTransitions(TestCaseWithFactory): | 393 | class TestBranchMergeProposalTransitions(TestCaseWithFactory): |
686 | 259 | """Test the state transitions of branch merge proposals.""" | 394 | """Test the state transitions of branch merge proposals.""" |
687 | 260 | 395 | ||
688 | @@ -1184,6 +1319,10 @@ class TestMergeProposalWebhooks(WithVCSScenarios, TestCaseWithFactory): | |||
689 | 1184 | def test_delete_triggers_webhooks(self): | 1319 | def test_delete_triggers_webhooks(self): |
690 | 1185 | # When an existing merge proposal is deleted, any relevant webhooks | 1320 | # When an existing merge proposal is deleted, any relevant webhooks |
691 | 1186 | # are triggered. | 1321 | # are triggered. |
692 | 1322 | self.useFixture(FeatureFixture({ | ||
693 | 1323 | GIT_CREATE_MP_VIRTUAL_REF: "on", | ||
694 | 1324 | BRANCH_MERGE_PROPOSAL_WEBHOOKS_FEATURE_FLAG: "on"})) | ||
695 | 1325 | hosting_fixture = self.useFixture(GitHostingFixture()) | ||
696 | 1187 | logger = self.useFixture(FakeLogger()) | 1326 | logger = self.useFixture(FakeLogger()) |
697 | 1188 | source = self.makeBranch() | 1327 | source = self.makeBranch() |
698 | 1189 | target = self.makeBranch(same_target_as=source) | 1328 | target = self.makeBranch(same_target_as=source) |
699 | @@ -1204,8 +1343,11 @@ class TestMergeProposalWebhooks(WithVCSScenarios, TestCaseWithFactory): | |||
700 | 1204 | old=MatchesDict(self.getExpectedPayload(proposal, redact=True))) | 1343 | old=MatchesDict(self.getExpectedPayload(proposal, redact=True))) |
701 | 1205 | proposal.deleteProposal() | 1344 | proposal.deleteProposal() |
702 | 1206 | delivery = hook.deliveries.one() | 1345 | delivery = hook.deliveries.one() |
703 | 1346 | self.assertIsNotNone(delivery) | ||
704 | 1207 | self.assertCorrectDelivery(expected_payload, hook, delivery) | 1347 | self.assertCorrectDelivery(expected_payload, hook, delivery) |
705 | 1208 | self.assertCorrectLogging(expected_redacted_payload, hook, logger) | 1348 | self.assertCorrectLogging(expected_redacted_payload, hook, logger) |
706 | 1349 | self.assertEqual( | ||
707 | 1350 | 1 if self.git else 0, hosting_fixture.deleteRef.call_count) | ||
708 | 1209 | 1351 | ||
709 | 1210 | 1352 | ||
710 | 1211 | class TestGetAddress(TestCaseWithFactory): | 1353 | class TestGetAddress(TestCaseWithFactory): |
711 | @@ -1506,6 +1648,18 @@ class TestBranchMergeProposalDeletion(TestCaseWithFactory): | |||
712 | 1506 | self.assertRaises( | 1648 | self.assertRaises( |
713 | 1507 | SQLObjectNotFound, BranchMergeProposalJob.get, job_id) | 1649 | SQLObjectNotFound, BranchMergeProposalJob.get, job_id) |
714 | 1508 | 1650 | ||
715 | 1651 | def test_deleteProposal_for_git_removes_virtual_ref(self): | ||
716 | 1652 | self.useFixture(FeatureFixture({GIT_CREATE_MP_VIRTUAL_REF: "on"})) | ||
717 | 1653 | hosting_fixture = self.useFixture(GitHostingFixture()) | ||
718 | 1654 | proposal = self.factory.makeBranchMergeProposalForGit() | ||
719 | 1655 | proposal.deleteProposal() | ||
720 | 1656 | |||
721 | 1657 | self.assertEqual(1, hosting_fixture.deleteRef.call_count) | ||
722 | 1658 | args = hosting_fixture.deleteRef.calls[0] | ||
723 | 1659 | self.assertEqual(( | ||
724 | 1660 | (proposal.target_git_repository.getInternalPath(), | ||
725 | 1661 | 'refs/merge/%s/head' % proposal.id), {'logger': None}), args) | ||
726 | 1662 | |||
727 | 1509 | 1663 | ||
728 | 1510 | class TestBranchMergeProposalBugs(WithVCSScenarios, TestCaseWithFactory): | 1664 | class TestBranchMergeProposalBugs(WithVCSScenarios, TestCaseWithFactory): |
729 | 1511 | 1665 | ||
730 | diff --git a/lib/lp/code/model/tests/test_githosting.py b/lib/lp/code/model/tests/test_githosting.py | |||
731 | index d966dbe..04f7a8e 100644 | |||
732 | --- a/lib/lp/code/model/tests/test_githosting.py | |||
733 | +++ b/lib/lp/code/model/tests/test_githosting.py | |||
734 | @@ -2,7 +2,7 @@ | |||
735 | 2 | # NOTE: The first line above must stay first; do not move the copyright | 2 | # NOTE: The first line above must stay first; do not move the copyright |
736 | 3 | # notice to the top. See http://www.python.org/dev/peps/pep-0263/. | 3 | # notice to the top. See http://www.python.org/dev/peps/pep-0263/. |
737 | 4 | # | 4 | # |
739 | 5 | # Copyright 2016-2019 Canonical Ltd. This software is licensed under the | 5 | # Copyright 2016-2020 Canonical Ltd. This software is licensed under the |
740 | 6 | # GNU Affero General Public License version 3 (see the file LICENSE). | 6 | # GNU Affero General Public License version 3 (see the file LICENSE). |
741 | 7 | 7 | ||
742 | 8 | """Unit tests for `GitHostingClient`. | 8 | """Unit tests for `GitHostingClient`. |
743 | @@ -37,12 +37,16 @@ from zope.interface import implementer | |||
744 | 37 | from zope.security.proxy import removeSecurityProxy | 37 | from zope.security.proxy import removeSecurityProxy |
745 | 38 | 38 | ||
746 | 39 | from lp.code.errors import ( | 39 | from lp.code.errors import ( |
747 | 40 | GitReferenceDeletionFault, | ||
748 | 40 | GitRepositoryBlobNotFound, | 41 | GitRepositoryBlobNotFound, |
749 | 41 | GitRepositoryCreationFault, | 42 | GitRepositoryCreationFault, |
750 | 42 | GitRepositoryDeletionFault, | 43 | GitRepositoryDeletionFault, |
751 | 43 | GitRepositoryScanFault, | 44 | GitRepositoryScanFault, |
752 | 45 | GitTargetError, | ||
753 | 46 | NoSuchGitReference, | ||
754 | 44 | ) | 47 | ) |
755 | 45 | from lp.code.interfaces.githosting import IGitHostingClient | 48 | from lp.code.interfaces.githosting import IGitHostingClient |
756 | 49 | from lp.code.model.githosting import RefCopyOperation | ||
757 | 46 | from lp.services.job.interfaces.job import ( | 50 | from lp.services.job.interfaces.job import ( |
758 | 47 | IRunnableJob, | 51 | IRunnableJob, |
759 | 48 | JobStatus, | 52 | JobStatus, |
760 | @@ -400,6 +404,46 @@ class TestGitHostingClient(TestCase): | |||
761 | 400 | " (256 vs 0)", | 404 | " (256 vs 0)", |
762 | 401 | self.client.getBlob, "123", "dir/path/file/name") | 405 | self.client.getBlob, "123", "dir/path/file/name") |
763 | 402 | 406 | ||
764 | 407 | def getCopyRefOperations(self): | ||
765 | 408 | return [ | ||
766 | 409 | RefCopyOperation("1a2b3c4", "999", "refs/merge/123"), | ||
767 | 410 | RefCopyOperation("9a8b7c6", "666", "refs/merge/989"), | ||
768 | 411 | ] | ||
769 | 412 | |||
770 | 413 | def test_copyRefs(self): | ||
771 | 414 | with self.mockRequests("POST", status=202): | ||
772 | 415 | self.client.copyRefs("123", self.getCopyRefOperations()) | ||
773 | 416 | self.assertRequest("repo/123/refs-copy", { | ||
774 | 417 | "operations": [ | ||
775 | 418 | { | ||
776 | 419 | "from": "1a2b3c4", | ||
777 | 420 | "to": {"repo": "999", "ref": "refs/merge/123"} | ||
778 | 421 | }, { | ||
779 | 422 | "from": "9a8b7c6", | ||
780 | 423 | "to": {"repo": "666", "ref": "refs/merge/989"} | ||
781 | 424 | } | ||
782 | 425 | ] | ||
783 | 426 | }, "POST") | ||
784 | 427 | |||
785 | 428 | def test_copyRefs_refs_not_found(self): | ||
786 | 429 | with self.mockRequests("POST", status=404): | ||
787 | 430 | self.assertRaisesWithContent( | ||
788 | 431 | GitTargetError, | ||
789 | 432 | "Could not find repository 123 or one of its refs", | ||
790 | 433 | self.client.copyRefs, "123", self.getCopyRefOperations()) | ||
791 | 434 | |||
792 | 435 | def test_deleteRef(self): | ||
793 | 436 | with self.mockRequests("DELETE", status=202): | ||
794 | 437 | self.client.deleteRef("123", "refs/merge/123") | ||
795 | 438 | self.assertRequest("repo/123/refs/merge/123", method="DELETE") | ||
796 | 439 | |||
797 | 440 | def test_deleteRef_refs_request_error(self): | ||
798 | 441 | with self.mockRequests("DELETE", status=500): | ||
799 | 442 | self.assertRaisesWithContent( | ||
800 | 443 | GitReferenceDeletionFault, | ||
801 | 444 | "Error deleting refs/merge/123 from repo 123: HTTP 500", | ||
802 | 445 | self.client.deleteRef, "123", "refs/merge/123") | ||
803 | 446 | |||
804 | 403 | def test_works_in_job(self): | 447 | def test_works_in_job(self): |
805 | 404 | # `GitHostingClient` is usable from a running job. | 448 | # `GitHostingClient` is usable from a running job. |
806 | 405 | @implementer(IRunnableJob) | 449 | @implementer(IRunnableJob) |
807 | diff --git a/lib/lp/code/model/tests/test_gitjob.py b/lib/lp/code/model/tests/test_gitjob.py | |||
808 | index 5964944..cc6ac49 100644 | |||
809 | --- a/lib/lp/code/model/tests/test_gitjob.py | |||
810 | +++ b/lib/lp/code/model/tests/test_gitjob.py | |||
811 | @@ -1,4 +1,4 @@ | |||
813 | 1 | # Copyright 2015-2019 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2015-2020 Canonical Ltd. This software is licensed under the |
814 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
815 | 3 | 3 | ||
816 | 4 | """Tests for `GitJob`s.""" | 4 | """Tests for `GitJob`s.""" |
817 | @@ -16,6 +16,7 @@ import hashlib | |||
818 | 16 | from fixtures import FakeLogger | 16 | from fixtures import FakeLogger |
819 | 17 | from lazr.lifecycle.snapshot import Snapshot | 17 | from lazr.lifecycle.snapshot import Snapshot |
820 | 18 | import pytz | 18 | import pytz |
821 | 19 | from storm.store import Store | ||
822 | 19 | from testtools.matchers import ( | 20 | from testtools.matchers import ( |
823 | 20 | ContainsDict, | 21 | ContainsDict, |
824 | 21 | Equals, | 22 | Equals, |
825 | @@ -24,9 +25,11 @@ from testtools.matchers import ( | |||
826 | 24 | MatchesStructure, | 25 | MatchesStructure, |
827 | 25 | ) | 26 | ) |
828 | 26 | import transaction | 27 | import transaction |
829 | 28 | from zope.component import getUtility | ||
830 | 27 | from zope.interface import providedBy | 29 | from zope.interface import providedBy |
831 | 28 | from zope.security.proxy import removeSecurityProxy | 30 | from zope.security.proxy import removeSecurityProxy |
832 | 29 | 31 | ||
833 | 32 | from lp.app.enums import InformationType | ||
834 | 30 | from lp.code.adapters.gitrepository import GitRepositoryDelta | 33 | from lp.code.adapters.gitrepository import GitRepositoryDelta |
835 | 31 | from lp.code.enums import ( | 34 | from lp.code.enums import ( |
836 | 32 | GitGranteeType, | 35 | GitGranteeType, |
837 | @@ -38,8 +41,10 @@ from lp.code.interfaces.branchmergeproposal import ( | |||
838 | 38 | from lp.code.interfaces.gitjob import ( | 41 | from lp.code.interfaces.gitjob import ( |
839 | 39 | IGitJob, | 42 | IGitJob, |
840 | 40 | IGitRefScanJob, | 43 | IGitRefScanJob, |
841 | 44 | IGitRepositoryVirtualRefsSyncJobSource, | ||
842 | 41 | IReclaimGitRepositorySpaceJob, | 45 | IReclaimGitRepositorySpaceJob, |
843 | 42 | ) | 46 | ) |
844 | 47 | from lp.code.interfaces.gitrepository import GIT_CREATE_MP_VIRTUAL_REF | ||
845 | 43 | from lp.code.model.gitjob import ( | 48 | from lp.code.model.gitjob import ( |
846 | 44 | describe_repository_delta, | 49 | describe_repository_delta, |
847 | 45 | GitJob, | 50 | GitJob, |
848 | @@ -484,5 +489,35 @@ class TestDescribeRepositoryDelta(TestCaseWithFactory): | |||
849 | 484 | snapshot, repository) | 489 | snapshot, repository) |
850 | 485 | 490 | ||
851 | 486 | 491 | ||
852 | 492 | class TestGitRepositoryVirtualRefsSyncJob(TestCaseWithFactory): | ||
853 | 493 | """Tests for `GitRepositoryVirtualRefsSyncJob`.""" | ||
854 | 494 | |||
855 | 495 | layer = ZopelessDatabaseLayer | ||
856 | 496 | |||
857 | 497 | def test_changing_repo_to_private_deletes_refs(self): | ||
858 | 498 | self.useFixture(FeatureFixture({GIT_CREATE_MP_VIRTUAL_REF: "on"})) | ||
859 | 499 | hosting_fixture = self.useFixture(GitHostingFixture()) | ||
860 | 500 | mp = self.factory.makeBranchMergeProposalForGit() | ||
861 | 501 | source_repo = mp.source_git_repository | ||
862 | 502 | target_repo = mp.target_git_repository | ||
863 | 503 | source_repo.transitionToInformationType( | ||
864 | 504 | InformationType.PRIVATESECURITY, source_repo.owner, False) | ||
865 | 505 | Store.of(source_repo).flush() | ||
866 | 506 | |||
867 | 507 | hosting_fixture.copyRefs.resetCalls() | ||
868 | 508 | hosting_fixture.deleteRef.resetCalls() | ||
869 | 509 | with dbuser("branchscanner"): | ||
870 | 510 | job_set = JobRunner.fromReady( | ||
871 | 511 | getUtility(IGitRepositoryVirtualRefsSyncJobSource)) | ||
872 | 512 | job_set.runAll() | ||
873 | 513 | self.assertEqual(1, len(job_set.completed_jobs)) | ||
874 | 514 | |||
875 | 515 | self.assertEqual(0, hosting_fixture.copyRefs.call_count) | ||
876 | 516 | self.assertEqual(1, hosting_fixture.deleteRef.call_count) | ||
877 | 517 | args, kwargs = hosting_fixture.deleteRef.calls[0] | ||
878 | 518 | self.assertEqual( | ||
879 | 519 | (target_repo.getInternalPath(), 'refs/merge/%s/head' % mp.id), | ||
880 | 520 | args) | ||
881 | 521 | |||
882 | 487 | # XXX cjwatson 2015-03-12: We should test that the jobs work via Celery too, | 522 | # XXX cjwatson 2015-03-12: We should test that the jobs work via Celery too, |
883 | 488 | # but that isn't feasible until we have a proper turnip fixture. | 523 | # but that isn't feasible until we have a proper turnip fixture. |
884 | diff --git a/lib/lp/code/model/tests/test_gitrepository.py b/lib/lp/code/model/tests/test_gitrepository.py | |||
885 | index 62bf865..6a9fa20 100644 | |||
886 | --- a/lib/lp/code/model/tests/test_gitrepository.py | |||
887 | +++ b/lib/lp/code/model/tests/test_gitrepository.py | |||
888 | @@ -18,6 +18,7 @@ from datetime import ( | |||
889 | 18 | import email | 18 | import email |
890 | 19 | from functools import partial | 19 | from functools import partial |
891 | 20 | import hashlib | 20 | import hashlib |
892 | 21 | import itertools | ||
893 | 21 | import json | 22 | import json |
894 | 22 | 23 | ||
895 | 23 | from breezy import urlutils | 24 | from breezy import urlutils |
896 | @@ -89,6 +90,7 @@ from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository | |||
897 | 89 | from lp.code.interfaces.gitjob import ( | 90 | from lp.code.interfaces.gitjob import ( |
898 | 90 | IGitRefScanJobSource, | 91 | IGitRefScanJobSource, |
899 | 91 | IGitRepositoryModifiedMailJobSource, | 92 | IGitRepositoryModifiedMailJobSource, |
900 | 93 | IGitRepositoryVirtualRefsSyncJobSource, | ||
901 | 92 | ) | 94 | ) |
902 | 93 | from lp.code.interfaces.gitlookup import IGitLookup | 95 | from lp.code.interfaces.gitlookup import IGitLookup |
903 | 94 | from lp.code.interfaces.gitnamespace import ( | 96 | from lp.code.interfaces.gitnamespace import ( |
904 | @@ -96,6 +98,7 @@ from lp.code.interfaces.gitnamespace import ( | |||
905 | 96 | IGitNamespaceSet, | 98 | IGitNamespaceSet, |
906 | 97 | ) | 99 | ) |
907 | 98 | from lp.code.interfaces.gitrepository import ( | 100 | from lp.code.interfaces.gitrepository import ( |
908 | 101 | GIT_CREATE_MP_VIRTUAL_REF, | ||
909 | 99 | IGitRepository, | 102 | IGitRepository, |
910 | 100 | IGitRepositorySet, | 103 | IGitRepositorySet, |
911 | 101 | IGitRepositoryView, | 104 | IGitRepositoryView, |
912 | @@ -113,6 +116,7 @@ from lp.code.model.branchmergeproposaljob import ( | |||
913 | 113 | ) | 116 | ) |
914 | 114 | from lp.code.model.codereviewcomment import CodeReviewComment | 117 | from lp.code.model.codereviewcomment import CodeReviewComment |
915 | 115 | from lp.code.model.gitactivity import GitActivity | 118 | from lp.code.model.gitactivity import GitActivity |
916 | 119 | from lp.code.model.githosting import RefCopyOperation | ||
917 | 116 | from lp.code.model.gitjob import ( | 120 | from lp.code.model.gitjob import ( |
918 | 117 | GitJob, | 121 | GitJob, |
919 | 118 | GitJobType, | 122 | GitJobType, |
920 | @@ -146,6 +150,7 @@ from lp.registry.interfaces.personociproject import IPersonOCIProjectFactory | |||
921 | 146 | from lp.registry.interfaces.personproduct import IPersonProductFactory | 150 | from lp.registry.interfaces.personproduct import IPersonProductFactory |
922 | 147 | from lp.registry.tests.test_accesspolicy import get_policies_for_artifact | 151 | from lp.registry.tests.test_accesspolicy import get_policies_for_artifact |
923 | 148 | from lp.services.authserver.xmlrpc import AuthServerAPIView | 152 | from lp.services.authserver.xmlrpc import AuthServerAPIView |
924 | 153 | from lp.services.compat import mock | ||
925 | 149 | from lp.services.config import config | 154 | from lp.services.config import config |
926 | 150 | from lp.services.database.constants import UTC_NOW | 155 | from lp.services.database.constants import UTC_NOW |
927 | 151 | from lp.services.database.interfaces import IStore | 156 | from lp.services.database.interfaces import IStore |
928 | @@ -180,6 +185,7 @@ from lp.testing import ( | |||
929 | 180 | verifyObject, | 185 | verifyObject, |
930 | 181 | ) | 186 | ) |
931 | 182 | from lp.testing.dbuser import dbuser | 187 | from lp.testing.dbuser import dbuser |
932 | 188 | from lp.testing.fixture import ZopeUtilityFixture | ||
933 | 183 | from lp.testing.layers import ( | 189 | from lp.testing.layers import ( |
934 | 184 | DatabaseFunctionalLayer, | 190 | DatabaseFunctionalLayer, |
935 | 185 | LaunchpadFunctionalLayer, | 191 | LaunchpadFunctionalLayer, |
936 | @@ -782,6 +788,7 @@ class TestGitRepositoryDeletion(TestCaseWithFactory): | |||
937 | 782 | # Make sure that the tests all flush the database changes. | 788 | # Make sure that the tests all flush the database changes. |
938 | 783 | self.addCleanup(Store.of(self.repository).flush) | 789 | self.addCleanup(Store.of(self.repository).flush) |
939 | 784 | login_person(self.user) | 790 | login_person(self.user) |
940 | 791 | self.hosting_fixture = self.useFixture(GitHostingFixture()) | ||
941 | 785 | 792 | ||
942 | 786 | def test_deletable(self): | 793 | def test_deletable(self): |
943 | 787 | # A newly created repository can be deleted without any problems. | 794 | # A newly created repository can be deleted without any problems. |
944 | @@ -823,7 +830,6 @@ class TestGitRepositoryDeletion(TestCaseWithFactory): | |||
945 | 823 | 830 | ||
946 | 824 | def test_code_import_does_not_disable_deletion(self): | 831 | def test_code_import_does_not_disable_deletion(self): |
947 | 825 | # A repository that has an attached code import can be deleted. | 832 | # A repository that has an attached code import can be deleted. |
948 | 826 | self.useFixture(GitHostingFixture()) | ||
949 | 827 | code_import = self.factory.makeCodeImport( | 833 | code_import = self.factory.makeCodeImport( |
950 | 828 | target_rcs_type=TargetRevisionControlSystems.GIT) | 834 | target_rcs_type=TargetRevisionControlSystems.GIT) |
951 | 829 | repository = code_import.git_repository | 835 | repository = code_import.git_repository |
952 | @@ -983,6 +989,7 @@ class TestGitRepositoryDeletionConsequences(TestCaseWithFactory): | |||
953 | 983 | # unsubscribe the repository owner here. | 989 | # unsubscribe the repository owner here. |
954 | 984 | self.repository.unsubscribe( | 990 | self.repository.unsubscribe( |
955 | 985 | self.repository.owner, self.repository.owner) | 991 | self.repository.owner, self.repository.owner) |
956 | 992 | self.hosting_fixture = self.useFixture(GitHostingFixture()) | ||
957 | 986 | 993 | ||
958 | 987 | def test_plain_repository(self): | 994 | def test_plain_repository(self): |
959 | 988 | # A fresh repository has no deletion requirements. | 995 | # A fresh repository has no deletion requirements. |
960 | @@ -1114,7 +1121,6 @@ class TestGitRepositoryDeletionConsequences(TestCaseWithFactory): | |||
961 | 1114 | 1121 | ||
962 | 1115 | def test_code_import_requirements(self): | 1122 | def test_code_import_requirements(self): |
963 | 1116 | # Code imports are not included explicitly in deletion requirements. | 1123 | # Code imports are not included explicitly in deletion requirements. |
964 | 1117 | self.useFixture(GitHostingFixture()) | ||
965 | 1118 | code_import = self.factory.makeCodeImport( | 1124 | code_import = self.factory.makeCodeImport( |
966 | 1119 | target_rcs_type=TargetRevisionControlSystems.GIT) | 1125 | target_rcs_type=TargetRevisionControlSystems.GIT) |
967 | 1120 | # Remove the implicit repository subscription first. | 1126 | # Remove the implicit repository subscription first. |
968 | @@ -1125,7 +1131,6 @@ class TestGitRepositoryDeletionConsequences(TestCaseWithFactory): | |||
969 | 1125 | 1131 | ||
970 | 1126 | def test_code_import_deletion(self): | 1132 | def test_code_import_deletion(self): |
971 | 1127 | # break_references allows deleting a code import repository. | 1133 | # break_references allows deleting a code import repository. |
972 | 1128 | self.useFixture(GitHostingFixture()) | ||
973 | 1129 | code_import = self.factory.makeCodeImport( | 1134 | code_import = self.factory.makeCodeImport( |
974 | 1130 | target_rcs_type=TargetRevisionControlSystems.GIT) | 1135 | target_rcs_type=TargetRevisionControlSystems.GIT) |
975 | 1131 | code_import_id = code_import.id | 1136 | code_import_id = code_import.id |
976 | @@ -1181,7 +1186,6 @@ class TestGitRepositoryDeletionConsequences(TestCaseWithFactory): | |||
977 | 1181 | 1186 | ||
978 | 1182 | def test_DeleteCodeImport(self): | 1187 | def test_DeleteCodeImport(self): |
979 | 1183 | # DeleteCodeImport.__call__ must delete the CodeImport. | 1188 | # DeleteCodeImport.__call__ must delete the CodeImport. |
980 | 1184 | self.useFixture(GitHostingFixture()) | ||
981 | 1185 | code_import = self.factory.makeCodeImport( | 1189 | code_import = self.factory.makeCodeImport( |
982 | 1186 | target_rcs_type=TargetRevisionControlSystems.GIT) | 1190 | target_rcs_type=TargetRevisionControlSystems.GIT) |
983 | 1187 | code_import_id = code_import.id | 1191 | code_import_id = code_import.id |
984 | @@ -1520,6 +1524,10 @@ class TestGitRepositoryRefs(TestCaseWithFactory): | |||
985 | 1520 | 1524 | ||
986 | 1521 | layer = DatabaseFunctionalLayer | 1525 | layer = DatabaseFunctionalLayer |
987 | 1522 | 1526 | ||
988 | 1527 | def setUp(self): | ||
989 | 1528 | super(TestGitRepositoryRefs, self).setUp() | ||
990 | 1529 | self.hosting_fixture = self.useFixture(GitHostingFixture()) | ||
991 | 1530 | |||
992 | 1523 | def test__convertRefInfo(self): | 1531 | def test__convertRefInfo(self): |
993 | 1524 | # _convertRefInfo converts a valid info dictionary. | 1532 | # _convertRefInfo converts a valid info dictionary. |
994 | 1525 | sha1 = unicode(hashlib.sha1("").hexdigest()) | 1533 | sha1 = unicode(hashlib.sha1("").hexdigest()) |
995 | @@ -1788,18 +1796,17 @@ class TestGitRepositoryRefs(TestCaseWithFactory): | |||
996 | 1788 | # planRefChanges excludes some ref prefixes by default, and can be | 1796 | # planRefChanges excludes some ref prefixes by default, and can be |
997 | 1789 | # configured otherwise. | 1797 | # configured otherwise. |
998 | 1790 | repository = self.factory.makeGitRepository() | 1798 | repository = self.factory.makeGitRepository() |
999 | 1791 | hosting_fixture = self.useFixture(GitHostingFixture()) | ||
1000 | 1792 | repository.planRefChanges("dummy") | 1799 | repository.planRefChanges("dummy") |
1001 | 1793 | self.assertEqual( | 1800 | self.assertEqual( |
1002 | 1794 | [{"exclude_prefixes": ["refs/changes/"]}], | 1801 | [{"exclude_prefixes": ["refs/changes/"]}], |
1005 | 1795 | hosting_fixture.getRefs.extract_kwargs()) | 1802 | self.hosting_fixture.getRefs.extract_kwargs()) |
1006 | 1796 | hosting_fixture.getRefs.calls = [] | 1803 | self.hosting_fixture.getRefs.calls = [] |
1007 | 1797 | self.pushConfig( | 1804 | self.pushConfig( |
1008 | 1798 | "codehosting", git_exclude_ref_prefixes="refs/changes/ refs/pull/") | 1805 | "codehosting", git_exclude_ref_prefixes="refs/changes/ refs/pull/") |
1009 | 1799 | repository.planRefChanges("dummy") | 1806 | repository.planRefChanges("dummy") |
1010 | 1800 | self.assertEqual( | 1807 | self.assertEqual( |
1011 | 1801 | [{"exclude_prefixes": ["refs/changes/", "refs/pull/"]}], | 1808 | [{"exclude_prefixes": ["refs/changes/", "refs/pull/"]}], |
1013 | 1802 | hosting_fixture.getRefs.extract_kwargs()) | 1809 | self.hosting_fixture.getRefs.extract_kwargs()) |
1014 | 1803 | 1810 | ||
1015 | 1804 | def test_fetchRefCommits(self): | 1811 | def test_fetchRefCommits(self): |
1016 | 1805 | # fetchRefCommits fetches detailed tip commit metadata for the | 1812 | # fetchRefCommits fetches detailed tip commit metadata for the |
1017 | @@ -1871,9 +1878,8 @@ class TestGitRepositoryRefs(TestCaseWithFactory): | |||
1018 | 1871 | def test_fetchRefCommits_empty(self): | 1878 | def test_fetchRefCommits_empty(self): |
1019 | 1872 | # If given an empty refs dictionary, fetchRefCommits returns early | 1879 | # If given an empty refs dictionary, fetchRefCommits returns early |
1020 | 1873 | # without contacting the hosting service. | 1880 | # without contacting the hosting service. |
1021 | 1874 | hosting_fixture = self.useFixture(GitHostingFixture()) | ||
1022 | 1875 | GitRepository.fetchRefCommits("dummy", {}) | 1881 | GitRepository.fetchRefCommits("dummy", {}) |
1024 | 1876 | self.assertEqual([], hosting_fixture.getCommits.calls) | 1882 | self.assertEqual([], self.hosting_fixture.getCommits.calls) |
1025 | 1877 | 1883 | ||
1026 | 1878 | def test_synchroniseRefs(self): | 1884 | def test_synchroniseRefs(self): |
1027 | 1879 | # synchroniseRefs copes with synchronising a repository where some | 1885 | # synchroniseRefs copes with synchronising a repository where some |
1028 | @@ -1916,7 +1922,6 @@ class TestGitRepositoryRefs(TestCaseWithFactory): | |||
1029 | 1916 | self.assertThat(repository.refs, MatchesSetwise(*matchers)) | 1922 | self.assertThat(repository.refs, MatchesSetwise(*matchers)) |
1030 | 1917 | 1923 | ||
1031 | 1918 | def test_set_default_branch(self): | 1924 | def test_set_default_branch(self): |
1032 | 1919 | hosting_fixture = self.useFixture(GitHostingFixture()) | ||
1033 | 1920 | repository = self.factory.makeGitRepository() | 1925 | repository = self.factory.makeGitRepository() |
1034 | 1921 | self.factory.makeGitRefs( | 1926 | self.factory.makeGitRefs( |
1035 | 1922 | repository=repository, | 1927 | repository=repository, |
1036 | @@ -1927,22 +1932,20 @@ class TestGitRepositoryRefs(TestCaseWithFactory): | |||
1037 | 1927 | self.assertEqual( | 1932 | self.assertEqual( |
1038 | 1928 | [((repository.getInternalPath(),), | 1933 | [((repository.getInternalPath(),), |
1039 | 1929 | {"default_branch": "refs/heads/new"})], | 1934 | {"default_branch": "refs/heads/new"})], |
1041 | 1930 | hosting_fixture.setProperties.calls) | 1935 | self.hosting_fixture.setProperties.calls) |
1042 | 1931 | self.assertEqual("refs/heads/new", repository.default_branch) | 1936 | self.assertEqual("refs/heads/new", repository.default_branch) |
1043 | 1932 | 1937 | ||
1044 | 1933 | def test_set_default_branch_unchanged(self): | 1938 | def test_set_default_branch_unchanged(self): |
1045 | 1934 | hosting_fixture = self.useFixture(GitHostingFixture()) | ||
1046 | 1935 | repository = self.factory.makeGitRepository() | 1939 | repository = self.factory.makeGitRepository() |
1047 | 1936 | self.factory.makeGitRefs( | 1940 | self.factory.makeGitRefs( |
1048 | 1937 | repository=repository, paths=["refs/heads/master"]) | 1941 | repository=repository, paths=["refs/heads/master"]) |
1049 | 1938 | removeSecurityProxy(repository)._default_branch = "refs/heads/master" | 1942 | removeSecurityProxy(repository)._default_branch = "refs/heads/master" |
1050 | 1939 | with person_logged_in(repository.owner): | 1943 | with person_logged_in(repository.owner): |
1051 | 1940 | repository.default_branch = "master" | 1944 | repository.default_branch = "master" |
1053 | 1941 | self.assertEqual([], hosting_fixture.setProperties.calls) | 1945 | self.assertEqual([], self.hosting_fixture.setProperties.calls) |
1054 | 1942 | self.assertEqual("refs/heads/master", repository.default_branch) | 1946 | self.assertEqual("refs/heads/master", repository.default_branch) |
1055 | 1943 | 1947 | ||
1056 | 1944 | def test_set_default_branch_imported(self): | 1948 | def test_set_default_branch_imported(self): |
1057 | 1945 | hosting_fixture = self.useFixture(GitHostingFixture()) | ||
1058 | 1946 | repository = self.factory.makeGitRepository( | 1949 | repository = self.factory.makeGitRepository( |
1059 | 1947 | repository_type=GitRepositoryType.IMPORTED) | 1950 | repository_type=GitRepositoryType.IMPORTED) |
1060 | 1948 | self.factory.makeGitRefs( | 1951 | self.factory.makeGitRefs( |
1061 | @@ -1955,7 +1958,7 @@ class TestGitRepositoryRefs(TestCaseWithFactory): | |||
1062 | 1955 | "Cannot modify non-hosted Git repository %s." % | 1958 | "Cannot modify non-hosted Git repository %s." % |
1063 | 1956 | repository.display_name, | 1959 | repository.display_name, |
1064 | 1957 | setattr, repository, "default_branch", "new") | 1960 | setattr, repository, "default_branch", "new") |
1066 | 1958 | self.assertEqual([], hosting_fixture.setProperties.calls) | 1961 | self.assertEqual([], self.hosting_fixture.setProperties.calls) |
1067 | 1959 | self.assertEqual("refs/heads/master", repository.default_branch) | 1962 | self.assertEqual("refs/heads/master", repository.default_branch) |
1068 | 1960 | 1963 | ||
1069 | 1961 | def test_exception_unset_default_branch(self): | 1964 | def test_exception_unset_default_branch(self): |
1070 | @@ -2576,18 +2579,59 @@ class TestGitRepositoryUpdateLandingTargets(TestCaseWithFactory): | |||
1071 | 2576 | 2579 | ||
1072 | 2577 | layer = DatabaseFunctionalLayer | 2580 | layer = DatabaseFunctionalLayer |
1073 | 2578 | 2581 | ||
1075 | 2579 | def test_schedules_diff_updates(self): | 2582 | def setUp(self): |
1076 | 2583 | super(TestGitRepositoryUpdateLandingTargets, self).setUp() | ||
1077 | 2584 | self.hosting_fixture = self.useFixture(GitHostingFixture()) | ||
1078 | 2585 | |||
1079 | 2586 | def assertSchedulesDiffUpdate(self, with_mp_virtual_ref): | ||
1080 | 2580 | """Create jobs for all merge proposals.""" | 2587 | """Create jobs for all merge proposals.""" |
1081 | 2581 | bmp1 = self.factory.makeBranchMergeProposalForGit() | 2588 | bmp1 = self.factory.makeBranchMergeProposalForGit() |
1082 | 2582 | bmp2 = self.factory.makeBranchMergeProposalForGit( | 2589 | bmp2 = self.factory.makeBranchMergeProposalForGit( |
1083 | 2583 | source_ref=bmp1.source_git_ref) | 2590 | source_ref=bmp1.source_git_ref) |
1085 | 2584 | jobs = bmp1.source_git_repository.updateLandingTargets( | 2591 | |
1086 | 2592 | # Only enable this virtual ref here, since | ||
1087 | 2593 | # self.factory.makeBranchMergeProposalForGit also tries to create | ||
1088 | 2594 | # the virtual refs. | ||
1089 | 2595 | if with_mp_virtual_ref: | ||
1090 | 2596 | self.useFixture(FeatureFixture({GIT_CREATE_MP_VIRTUAL_REF: "on"})) | ||
1091 | 2597 | else: | ||
1092 | 2598 | self.useFixture(FeatureFixture({GIT_CREATE_MP_VIRTUAL_REF: ""})) | ||
1093 | 2599 | jobs, ref_copies = bmp1.source_git_repository.updateLandingTargets( | ||
1094 | 2585 | [bmp1.source_git_path]) | 2600 | [bmp1.source_git_path]) |
1095 | 2586 | self.assertEqual(2, len(jobs)) | 2601 | self.assertEqual(2, len(jobs)) |
1096 | 2587 | bmps_to_update = [ | 2602 | bmps_to_update = [ |
1097 | 2588 | removeSecurityProxy(job).branch_merge_proposal for job in jobs] | 2603 | removeSecurityProxy(job).branch_merge_proposal for job in jobs] |
1098 | 2589 | self.assertContentEqual([bmp1, bmp2], bmps_to_update) | 2604 | self.assertContentEqual([bmp1, bmp2], bmps_to_update) |
1099 | 2590 | 2605 | ||
1100 | 2606 | if not with_mp_virtual_ref: | ||
1101 | 2607 | self.assertEqual(0, self.hosting_fixture.copyRefs.call_count) | ||
1102 | 2608 | else: | ||
1103 | 2609 | self.assertEqual(1, self.hosting_fixture.copyRefs.call_count) | ||
1104 | 2610 | args, kwargs = self.hosting_fixture.copyRefs.calls[0] | ||
1105 | 2611 | self.assertEqual({}, kwargs) | ||
1106 | 2612 | self.assertEqual(2, len(args)) | ||
1107 | 2613 | path, operations = args | ||
1108 | 2614 | self.assertEqual( | ||
1109 | 2615 | path, bmp1.source_git_repository.getInternalPath()) | ||
1110 | 2616 | self.assertThat(operations[0], MatchesStructure( | ||
1111 | 2617 | source_ref=Equals(bmp1.source_git_commit_sha1), | ||
1112 | 2618 | target_repo=Equals( | ||
1113 | 2619 | bmp1.target_git_repository.getInternalPath()), | ||
1114 | 2620 | target_ref=Equals("refs/merge/%s/head" % bmp1.id), | ||
1115 | 2621 | )) | ||
1116 | 2622 | self.assertThat(operations[1], MatchesStructure( | ||
1117 | 2623 | source_ref=Equals(bmp2.source_git_commit_sha1), | ||
1118 | 2624 | target_repo=Equals( | ||
1119 | 2625 | bmp2.target_git_repository.getInternalPath()), | ||
1120 | 2626 | target_ref=Equals("refs/merge/%s/head" % bmp2.id), | ||
1121 | 2627 | )) | ||
1122 | 2628 | |||
1123 | 2629 | def test_schedules_diff_updates_with_mp_virtual_ref(self): | ||
1124 | 2630 | self.assertSchedulesDiffUpdate(True) | ||
1125 | 2631 | |||
1126 | 2632 | def test_schedules_diff_updates_without_mp_virtual_ref(self): | ||
1127 | 2633 | self.assertSchedulesDiffUpdate(False) | ||
1128 | 2634 | |||
1129 | 2591 | def test_ignores_final(self): | 2635 | def test_ignores_final(self): |
1130 | 2592 | """Diffs for proposals in final states aren't updated.""" | 2636 | """Diffs for proposals in final states aren't updated.""" |
1131 | 2593 | [source_ref] = self.factory.makeGitRefs() | 2637 | [source_ref] = self.factory.makeGitRefs() |
1132 | @@ -2599,8 +2643,15 @@ class TestGitRepositoryUpdateLandingTargets(TestCaseWithFactory): | |||
1133 | 2599 | for bmp in source_ref.landing_targets: | 2643 | for bmp in source_ref.landing_targets: |
1134 | 2600 | if bmp.queue_status not in FINAL_STATES: | 2644 | if bmp.queue_status not in FINAL_STATES: |
1135 | 2601 | removeSecurityProxy(bmp).deleteProposal() | 2645 | removeSecurityProxy(bmp).deleteProposal() |
1137 | 2602 | jobs = source_ref.repository.updateLandingTargets([source_ref.path]) | 2646 | |
1138 | 2647 | # Enable the feature here, since factory.makeBranchMergeProposalForGit | ||
1139 | 2648 | # would also trigger the copy refs call. | ||
1140 | 2649 | self.useFixture(FeatureFixture({GIT_CREATE_MP_VIRTUAL_REF: "on"})) | ||
1141 | 2650 | jobs, ref_copies = source_ref.repository.updateLandingTargets( | ||
1142 | 2651 | [source_ref.path]) | ||
1143 | 2603 | self.assertEqual(0, len(jobs)) | 2652 | self.assertEqual(0, len(jobs)) |
1144 | 2653 | self.assertEqual(0, len(ref_copies)) | ||
1145 | 2654 | self.assertEqual(0, self.hosting_fixture.copyRefs.call_count) | ||
1146 | 2604 | 2655 | ||
1147 | 2605 | 2656 | ||
1148 | 2606 | class TestGitRepositoryMarkRecipesStale(TestCaseWithFactory): | 2657 | class TestGitRepositoryMarkRecipesStale(TestCaseWithFactory): |
1149 | @@ -4616,3 +4667,61 @@ class TestGitRepositoryMacaroonIssuer(MacaroonTestMixin, TestCaseWithFactory): | |||
1150 | 4616 | ["Caveat check for '%s' failed." % | 4667 | ["Caveat check for '%s' failed." % |
1151 | 4617 | find_caveats_by_name(macaroon2, "lp.expires")[0].caveat_id], | 4668 | find_caveats_by_name(macaroon2, "lp.expires")[0].caveat_id], |
1152 | 4618 | issuer, macaroon2, repository, user=repository.owner) | 4669 | issuer, macaroon2, repository, user=repository.owner) |
1153 | 4670 | |||
1154 | 4671 | |||
1155 | 4672 | class TestGitRepositoryPrivacyChangeSyncVirtualRefs(TestCaseWithFactory): | ||
1156 | 4673 | layer = DatabaseFunctionalLayer | ||
1157 | 4674 | |||
1158 | 4675 | def assertChangePrivacyTriggersSync( | ||
1159 | 4676 | self, from_list, to_list, should_trigger_sync=True): | ||
1160 | 4677 | """Runs repository.transitionToInformationType from every item in | ||
1161 | 4678 | `from_list` to each item in `to_list`, and checks if the virtual | ||
1162 | 4679 | refs sync was triggered or not, depending on `should_trigger_sync`.""" | ||
1163 | 4680 | sync_job = mock.Mock() | ||
1164 | 4681 | self.useFixture(ZopeUtilityFixture( | ||
1165 | 4682 | sync_job, IGitRepositoryVirtualRefsSyncJobSource)) | ||
1166 | 4683 | |||
1167 | 4684 | admin = self.factory.makeAdministrator() | ||
1168 | 4685 | login_person(admin) | ||
1169 | 4686 | for from_type, to_type in itertools.product(from_list, to_list): | ||
1170 | 4687 | if from_type == to_type: | ||
1171 | 4688 | continue | ||
1172 | 4689 | repository = self.factory.makeGitRepository() | ||
1173 | 4690 | naked_repo = removeSecurityProxy(repository) | ||
1174 | 4691 | naked_repo.information_type = from_type | ||
1175 | 4692 | # Skip access policy reconciliation. | ||
1176 | 4693 | naked_repo._reconcileAccess = mock.Mock() | ||
1177 | 4694 | naked_repo.transitionToInformationType(to_type, admin, False) | ||
1178 | 4695 | |||
1179 | 4696 | if should_trigger_sync: | ||
1180 | 4697 | sync_job.create.assert_called_with(repository) | ||
1181 | 4698 | else: | ||
1182 | 4699 | self.assertEqual( | ||
1183 | 4700 | 0, sync_job.create.call_count, | ||
1184 | 4701 | "Changing from %s to %s should't trigger vrefs sync" | ||
1185 | 4702 | % (from_type, to_type)) | ||
1186 | 4703 | sync_job.reset_mock() | ||
1187 | 4704 | |||
1188 | 4705 | def test_setting_repo_public_triggers_ref_sync_job(self): | ||
1189 | 4706 | self.assertChangePrivacyTriggersSync( | ||
1190 | 4707 | PRIVATE_INFORMATION_TYPES, | ||
1191 | 4708 | PUBLIC_INFORMATION_TYPES, | ||
1192 | 4709 | should_trigger_sync=True) | ||
1193 | 4710 | |||
1194 | 4711 | def test_setting_repo_private_triggers_ref_sync_job(self): | ||
1195 | 4712 | self.assertChangePrivacyTriggersSync( | ||
1196 | 4713 | PUBLIC_INFORMATION_TYPES, | ||
1197 | 4714 | PRIVATE_INFORMATION_TYPES, | ||
1198 | 4715 | should_trigger_sync=True) | ||
1199 | 4716 | |||
1200 | 4717 | def test_keeping_repo_private_dont_trigger_ref_sync_job(self): | ||
1201 | 4718 | self.assertChangePrivacyTriggersSync( | ||
1202 | 4719 | PRIVATE_INFORMATION_TYPES, | ||
1203 | 4720 | PRIVATE_INFORMATION_TYPES, | ||
1204 | 4721 | should_trigger_sync=False) | ||
1205 | 4722 | |||
1206 | 4723 | def test_keeping_repo_public_dont_trigger_ref_sync_job(self): | ||
1207 | 4724 | self.assertChangePrivacyTriggersSync( | ||
1208 | 4725 | PUBLIC_INFORMATION_TYPES, | ||
1209 | 4726 | PUBLIC_INFORMATION_TYPES, | ||
1210 | 4727 | should_trigger_sync=False) | ||
1211 | diff --git a/lib/lp/code/subscribers/branchmergeproposal.py b/lib/lp/code/subscribers/branchmergeproposal.py | |||
1212 | index a214c8f..b394388 100644 | |||
1213 | --- a/lib/lp/code/subscribers/branchmergeproposal.py | |||
1214 | +++ b/lib/lp/code/subscribers/branchmergeproposal.py | |||
1215 | @@ -1,4 +1,4 @@ | |||
1217 | 1 | # Copyright 2010-2016 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2010-2020 Canonical Ltd. This software is licensed under the |
1218 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1219 | 3 | 3 | ||
1220 | 4 | """Event subscribers for branch merge proposals.""" | 4 | """Event subscribers for branch merge proposals.""" |
1221 | @@ -63,9 +63,13 @@ def _trigger_webhook(merge_proposal, payload): | |||
1222 | 63 | def merge_proposal_created(merge_proposal, event): | 63 | def merge_proposal_created(merge_proposal, event): |
1223 | 64 | """A new merge proposal has been created. | 64 | """A new merge proposal has been created. |
1224 | 65 | 65 | ||
1226 | 66 | Create a job to update the diff for the merge proposal; trigger webhooks. | 66 | Create a job to update the diff for the merge proposal; trigger webhooks |
1227 | 67 | and copy virtual refs as needed. | ||
1228 | 67 | """ | 68 | """ |
1229 | 68 | getUtility(IUpdatePreviewDiffJobSource).create(merge_proposal) | 69 | getUtility(IUpdatePreviewDiffJobSource).create(merge_proposal) |
1230 | 70 | |||
1231 | 71 | merge_proposal.syncGitHostingVirtualRefs() | ||
1232 | 72 | |||
1233 | 69 | if getFeatureFlag(BRANCH_MERGE_PROPOSAL_WEBHOOKS_FEATURE_FLAG): | 73 | if getFeatureFlag(BRANCH_MERGE_PROPOSAL_WEBHOOKS_FEATURE_FLAG): |
1234 | 70 | payload = { | 74 | payload = { |
1235 | 71 | "action": "created", | 75 | "action": "created", |
1236 | diff --git a/lib/lp/code/tests/helpers.py b/lib/lp/code/tests/helpers.py | |||
1237 | index 0e784f0..90a7a87 100644 | |||
1238 | --- a/lib/lp/code/tests/helpers.py | |||
1239 | +++ b/lib/lp/code/tests/helpers.py | |||
1240 | @@ -368,6 +368,8 @@ class GitHostingFixture(fixtures.Fixture): | |||
1241 | 368 | self.getBlob = FakeMethod(result=blob) | 368 | self.getBlob = FakeMethod(result=blob) |
1242 | 369 | self.delete = FakeMethod() | 369 | self.delete = FakeMethod() |
1243 | 370 | self.disable_memcache = disable_memcache | 370 | self.disable_memcache = disable_memcache |
1244 | 371 | self.copyRefs = FakeMethod() | ||
1245 | 372 | self.deleteRef = FakeMethod() | ||
1246 | 371 | 373 | ||
1247 | 372 | def _setUp(self): | 374 | def _setUp(self): |
1248 | 373 | self.useFixture(ZopeUtilityFixture(self, IGitHostingClient)) | 375 | self.useFixture(ZopeUtilityFixture(self, IGitHostingClient)) |
1249 | diff --git a/lib/lp/services/config/schema-lazr.conf b/lib/lp/services/config/schema-lazr.conf | |||
1250 | index bb8afcd..3921da2 100644 | |||
1251 | --- a/lib/lp/services/config/schema-lazr.conf | |||
1252 | +++ b/lib/lp/services/config/schema-lazr.conf | |||
1253 | @@ -1828,6 +1828,10 @@ module: lp.code.interfaces.gitjob | |||
1254 | 1828 | dbuser: send-branch-mail | 1828 | dbuser: send-branch-mail |
1255 | 1829 | crontab_group: MAIN | 1829 | crontab_group: MAIN |
1256 | 1830 | 1830 | ||
1257 | 1831 | [IGitRepositoryVirtualRefsSyncJobSource] | ||
1258 | 1832 | module: lp.code.interfaces.gitjob | ||
1259 | 1833 | dbuser: branchscanner | ||
1260 | 1834 | |||
1261 | 1831 | [IInitializeDistroSeriesJobSource] | 1835 | [IInitializeDistroSeriesJobSource] |
1262 | 1832 | module: lp.soyuz.interfaces.distributionjob | 1836 | module: lp.soyuz.interfaces.distributionjob |
1263 | 1833 | dbuser: initializedistroseries | 1837 | dbuser: initializedistroseries |
1264 | diff --git a/lib/lp/testing/fakemethod.py b/lib/lp/testing/fakemethod.py | |||
1265 | index 4bba8d3..b4895ce 100644 | |||
1266 | --- a/lib/lp/testing/fakemethod.py | |||
1267 | +++ b/lib/lp/testing/fakemethod.py | |||
1268 | @@ -1,4 +1,4 @@ | |||
1270 | 1 | # Copyright 2009-2011 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2020 Canonical Ltd. This software is licensed under the |
1271 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1272 | 3 | 3 | ||
1273 | 4 | __metaclass__ = type | 4 | __metaclass__ = type |
1274 | @@ -57,3 +57,6 @@ class FakeMethod: | |||
1275 | 57 | def extract_kwargs(self): | 57 | def extract_kwargs(self): |
1276 | 58 | """Return just the calls' keyword-arguments dicts.""" | 58 | """Return just the calls' keyword-arguments dicts.""" |
1277 | 59 | return [kwargs for args, kwargs in self.calls] | 59 | return [kwargs for args, kwargs in self.calls] |
1278 | 60 | |||
1279 | 61 | def resetCalls(self): | ||
1280 | 62 | self.calls = [] |