Merge lp:~cjwatson/launchpad/snap-git-url into lp:launchpad
- snap-git-url
- Merge into devel
Proposed by
Colin Watson
Status: | Merged |
---|---|
Merged at revision: | 18288 |
Proposed branch: | lp:~cjwatson/launchpad/snap-git-url |
Merge into: | lp:launchpad |
Prerequisite: | lp:~cjwatson/launchpad/git-ref-remote |
Diff against target: |
574 lines (+269/-23) 9 files modified
lib/lp/snappy/browser/snap.py (+1/-1) lib/lp/snappy/browser/tests/test_snap.py (+50/-0) lib/lp/snappy/browser/tests/test_snaplisting.py (+12/-5) lib/lp/snappy/interfaces/snap.py (+37/-5) lib/lp/snappy/model/snap.py (+60/-5) lib/lp/snappy/model/snapbuildbehaviour.py (+6/-2) lib/lp/snappy/tests/test_snap.py (+76/-1) lib/lp/snappy/tests/test_snapbuildbehaviour.py (+24/-2) lib/lp/testing/matchers.py (+3/-2) |
To merge this branch: | bzr merge lp:~cjwatson/launchpad/snap-git-url |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+312348@code.launchpad.net |
Commit message
Allow building snaps from an external Git repository.
Description of the change
The UI for this works to the extent that it displays something approximately reasonable and lets you edit such snaps, but the only reasonable way to create them is via the API [1] since the +new-snap view is based on Branch or GitRef. That's OK for the moment since the immediate audience for this is the separate build.snapcraft.io service.
https:/
[1] Technically, you can create one based on some LP-hosted repository and then edit it into the shape you want. I don't count that as a reasonable way.
To post a comment you must log in.
Revision history for this message
William Grant (wgrant) : | # |
review:
Approve
(code)
Revision history for this message
Colin Watson (cjwatson) : | # |
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lib/lp/snappy/browser/snap.py' | |||
2 | --- lib/lp/snappy/browser/snap.py 2016-11-07 18:14:32 +0000 | |||
3 | +++ lib/lp/snappy/browser/snap.py 2016-12-05 19:02:57 +0000 | |||
4 | @@ -700,7 +700,7 @@ | |||
5 | 700 | custom_widget('store_distro_series', LaunchpadRadioWidget) | 700 | custom_widget('store_distro_series', LaunchpadRadioWidget) |
6 | 701 | custom_widget('store_channels', LabeledMultiCheckBoxWidget) | 701 | custom_widget('store_channels', LabeledMultiCheckBoxWidget) |
7 | 702 | custom_widget('vcs', LaunchpadRadioWidget) | 702 | custom_widget('vcs', LaunchpadRadioWidget) |
9 | 703 | custom_widget('git_ref', GitRefWidget) | 703 | custom_widget('git_ref', GitRefWidget, allow_external=True) |
10 | 704 | custom_widget('auto_build_archive', SnapArchiveWidget) | 704 | custom_widget('auto_build_archive', SnapArchiveWidget) |
11 | 705 | custom_widget('auto_build_pocket', LaunchpadDropdownWidget) | 705 | custom_widget('auto_build_pocket', LaunchpadDropdownWidget) |
12 | 706 | 706 | ||
13 | 707 | 707 | ||
14 | === modified file 'lib/lp/snappy/browser/tests/test_snap.py' | |||
15 | --- lib/lp/snappy/browser/tests/test_snap.py 2016-11-07 18:14:32 +0000 | |||
16 | +++ lib/lp/snappy/browser/tests/test_snap.py 2016-12-05 19:02:57 +0000 | |||
17 | @@ -724,6 +724,29 @@ | |||
18 | 724 | "name.", | 724 | "name.", |
19 | 725 | extract_text(find_tags_by_class(browser.contents, "message")[1])) | 725 | extract_text(find_tags_by_class(browser.contents, "message")[1])) |
20 | 726 | 726 | ||
21 | 727 | def test_edit_snap_git_url(self): | ||
22 | 728 | series = self.factory.makeUbuntuDistroSeries() | ||
23 | 729 | with admin_logged_in(): | ||
24 | 730 | snappy_series = self.factory.makeSnappySeries( | ||
25 | 731 | usable_distro_series=[series]) | ||
26 | 732 | old_ref = self.factory.makeGitRefRemote() | ||
27 | 733 | new_ref = self.factory.makeGitRefRemote() | ||
28 | 734 | new_repository_url = new_ref.repository_url | ||
29 | 735 | new_path = new_ref.path | ||
30 | 736 | snap = self.factory.makeSnap( | ||
31 | 737 | registrant=self.person, owner=self.person, distroseries=series, | ||
32 | 738 | git_ref=old_ref, store_series=snappy_series) | ||
33 | 739 | browser = self.getViewBrowser(snap, user=self.person) | ||
34 | 740 | browser.getLink("Edit snap package").click() | ||
35 | 741 | browser.getControl("Git repository").value = new_repository_url | ||
36 | 742 | browser.getControl("Git branch").value = new_path | ||
37 | 743 | browser.getControl("Update snap package").click() | ||
38 | 744 | login_person(self.person) | ||
39 | 745 | content = find_main_content(browser.contents) | ||
40 | 746 | self.assertThat( | ||
41 | 747 | "Source:\n%s\nEdit snap package" % new_ref.display_name, | ||
42 | 748 | MatchesTagText(content, "source")) | ||
43 | 749 | |||
44 | 727 | def setUpDistroSeries(self): | 750 | def setUpDistroSeries(self): |
45 | 728 | """Set up a distroseries with some available processors.""" | 751 | """Set up a distroseries with some available processors.""" |
46 | 729 | distroseries = self.factory.makeUbuntuDistroSeries() | 752 | distroseries = self.factory.makeUbuntuDistroSeries() |
47 | @@ -1259,6 +1282,33 @@ | |||
48 | 1259 | Primary Archive for Ubuntu Linux | 1282 | Primary Archive for Ubuntu Linux |
49 | 1260 | """, self.getMainText(build.snap)) | 1283 | """, self.getMainText(build.snap)) |
50 | 1261 | 1284 | ||
51 | 1285 | def test_index_git_url(self): | ||
52 | 1286 | ref = self.factory.makeGitRefRemote( | ||
53 | 1287 | repository_url=u"https://git.example.org/foo", | ||
54 | 1288 | path=u"refs/heads/master") | ||
55 | 1289 | snap = self.makeSnap(git_ref=ref) | ||
56 | 1290 | build = self.makeBuild( | ||
57 | 1291 | snap=snap, status=BuildStatus.FULLYBUILT, | ||
58 | 1292 | duration=timedelta(minutes=30)) | ||
59 | 1293 | self.assertTextMatchesExpressionIgnoreWhitespace("""\ | ||
60 | 1294 | Snap packages snap-name | ||
61 | 1295 | .* | ||
62 | 1296 | Snap package information | ||
63 | 1297 | Owner: Test Person | ||
64 | 1298 | Distribution series: Ubuntu Shiny | ||
65 | 1299 | Source: https://git.example.org/foo master | ||
66 | 1300 | Build schedule: \(\?\) | ||
67 | 1301 | Built on request | ||
68 | 1302 | Source archive for automatic builds: | ||
69 | 1303 | Pocket for automatic builds: | ||
70 | 1304 | Builds of this snap package are not automatically uploaded to | ||
71 | 1305 | the store. | ||
72 | 1306 | Latest builds | ||
73 | 1307 | Status When complete Architecture Archive | ||
74 | 1308 | Successfully built 30 minutes ago i386 | ||
75 | 1309 | Primary Archive for Ubuntu Linux | ||
76 | 1310 | """, self.getMainText(build.snap)) | ||
77 | 1311 | |||
78 | 1262 | def test_index_success_with_buildlog(self): | 1312 | def test_index_success_with_buildlog(self): |
79 | 1263 | # The build log is shown if it is there. | 1313 | # The build log is shown if it is there. |
80 | 1264 | build = self.makeBuild( | 1314 | build = self.makeBuild( |
81 | 1265 | 1315 | ||
82 | === modified file 'lib/lp/snappy/browser/tests/test_snaplisting.py' | |||
83 | --- lib/lp/snappy/browser/tests/test_snaplisting.py 2016-09-07 11:12:58 +0000 | |||
84 | +++ lib/lp/snappy/browser/tests/test_snaplisting.py 2016-12-05 19:02:57 +0000 | |||
85 | @@ -11,6 +11,7 @@ | |||
86 | 11 | from lp.code.tests.helpers import GitHostingFixture | 11 | from lp.code.tests.helpers import GitHostingFixture |
87 | 12 | from lp.services.database.constants import ( | 12 | from lp.services.database.constants import ( |
88 | 13 | ONE_DAY_AGO, | 13 | ONE_DAY_AGO, |
89 | 14 | SEVEN_DAYS_AGO, | ||
90 | 14 | UTC_NOW, | 15 | UTC_NOW, |
91 | 15 | ) | 16 | ) |
92 | 16 | from lp.services.webapp import canonical_url | 17 | from lp.services.webapp import canonical_url |
93 | @@ -107,16 +108,22 @@ | |||
94 | 107 | owner = self.factory.makePerson(displayname="Snap Owner") | 108 | owner = self.factory.makePerson(displayname="Snap Owner") |
95 | 108 | self.factory.makeSnap( | 109 | self.factory.makeSnap( |
96 | 109 | registrant=owner, owner=owner, branch=self.factory.makeAnyBranch(), | 110 | registrant=owner, owner=owner, branch=self.factory.makeAnyBranch(), |
97 | 111 | date_created=SEVEN_DAYS_AGO) | ||
98 | 112 | [ref] = self.factory.makeGitRefs() | ||
99 | 113 | self.factory.makeSnap( | ||
100 | 114 | registrant=owner, owner=owner, git_ref=ref, | ||
101 | 110 | date_created=ONE_DAY_AGO) | 115 | date_created=ONE_DAY_AGO) |
103 | 111 | [ref] = self.factory.makeGitRefs() | 116 | remote_ref = self.factory.makeGitRefRemote() |
104 | 112 | self.factory.makeSnap( | 117 | self.factory.makeSnap( |
106 | 113 | registrant=owner, owner=owner, git_ref=ref, date_created=UTC_NOW) | 118 | registrant=owner, owner=owner, git_ref=remote_ref, |
107 | 119 | date_created=UTC_NOW) | ||
108 | 114 | text = self.getMainText(owner, "+snaps") | 120 | text = self.getMainText(owner, "+snaps") |
109 | 115 | self.assertTextMatchesExpressionIgnoreWhitespace(""" | 121 | self.assertTextMatchesExpressionIgnoreWhitespace(""" |
110 | 116 | Snap packages for Snap Owner | 122 | Snap packages for Snap Owner |
114 | 117 | Name Source Registered | 123 | Name Source Registered |
115 | 118 | snap-name.* ~.*:.* .* | 124 | snap-name.* http://.* path-.* .* |
116 | 119 | snap-name.* lp:.* .*""", text) | 125 | snap-name.* ~.*:.* .* |
117 | 126 | snap-name.* lp:.* .*""", text) | ||
118 | 120 | 127 | ||
119 | 121 | def test_project_snap_listing(self): | 128 | def test_project_snap_listing(self): |
120 | 122 | # We can see snap packages for a project. | 129 | # We can see snap packages for a project. |
121 | 123 | 130 | ||
122 | === modified file 'lib/lp/snappy/interfaces/snap.py' | |||
123 | --- lib/lp/snappy/interfaces/snap.py 2016-11-22 02:13:11 +0000 | |||
124 | +++ lib/lp/snappy/interfaces/snap.py 2016-12-05 19:02:57 +0000 | |||
125 | @@ -85,6 +85,7 @@ | |||
126 | 85 | from lp.services.fields import ( | 85 | from lp.services.fields import ( |
127 | 86 | PersonChoice, | 86 | PersonChoice, |
128 | 87 | PublicPersonChoice, | 87 | PublicPersonChoice, |
129 | 88 | URIField, | ||
130 | 88 | ) | 89 | ) |
131 | 89 | from lp.services.webhooks.interfaces import IWebhookTarget | 90 | from lp.services.webhooks.interfaces import IWebhookTarget |
132 | 90 | from lp.snappy.interfaces.snappyseries import ( | 91 | from lp.snappy.interfaces.snappyseries import ( |
133 | @@ -380,6 +381,18 @@ | |||
134 | 380 | "A Git repository with a branch containing a snapcraft.yaml " | 381 | "A Git repository with a branch containing a snapcraft.yaml " |
135 | 381 | "recipe at the top level."))) | 382 | "recipe at the top level."))) |
136 | 382 | 383 | ||
137 | 384 | git_repository_url = exported(URIField( | ||
138 | 385 | title=_("Git repository URL"), required=False, readonly=True, | ||
139 | 386 | description=_( | ||
140 | 387 | "The URL of a Git repository with a branch containing a " | ||
141 | 388 | "snapcraft.yaml recipe at the top level."), | ||
142 | 389 | allowed_schemes=["git", "http", "https"], | ||
143 | 390 | allow_userinfo=True, | ||
144 | 391 | allow_port=True, | ||
145 | 392 | allow_query=False, | ||
146 | 393 | allow_fragment=False, | ||
147 | 394 | trailing_slash=False)) | ||
148 | 395 | |||
149 | 383 | git_path = exported(TextLine( | 396 | git_path = exported(TextLine( |
150 | 384 | title=_("Git branch path"), required=False, readonly=True, | 397 | title=_("Git branch path"), required=False, readonly=True, |
151 | 385 | description=_( | 398 | description=_( |
152 | @@ -503,11 +516,13 @@ | |||
153 | 503 | @export_factory_operation( | 516 | @export_factory_operation( |
154 | 504 | ISnap, [ | 517 | ISnap, [ |
155 | 505 | "owner", "distro_series", "name", "description", "branch", | 518 | "owner", "distro_series", "name", "description", "branch", |
157 | 506 | "git_ref", "auto_build", "auto_build_archive", "auto_build_pocket", | 519 | "git_repository", "git_repository_url", "git_path", "git_ref", |
158 | 520 | "auto_build", "auto_build_archive", "auto_build_pocket", | ||
159 | 507 | "private"]) | 521 | "private"]) |
160 | 508 | @operation_for_version("devel") | 522 | @operation_for_version("devel") |
161 | 509 | def new(registrant, owner, distro_series, name, description=None, | 523 | def new(registrant, owner, distro_series, name, description=None, |
163 | 510 | branch=None, git_ref=None, auto_build=False, | 524 | branch=None, git_repository=None, git_repository_url=None, |
164 | 525 | git_path=None, git_ref=None, auto_build=False, | ||
165 | 511 | auto_build_archive=None, auto_build_pocket=None, | 526 | auto_build_archive=None, auto_build_pocket=None, |
166 | 512 | require_virtualized=True, processors=None, date_created=None, | 527 | require_virtualized=True, processors=None, date_created=None, |
167 | 513 | private=False, store_upload=False, store_series=None, | 528 | private=False, store_upload=False, store_series=None, |
168 | @@ -540,7 +555,7 @@ | |||
169 | 540 | 555 | ||
170 | 541 | :param person: An `IPerson`. | 556 | :param person: An `IPerson`. |
171 | 542 | :param visible_by_user: If not None, only return packages visible by | 557 | :param visible_by_user: If not None, only return packages visible by |
173 | 543 | this user. | 558 | this user; otherwise, only return publicly-visible packages. |
174 | 544 | """ | 559 | """ |
175 | 545 | 560 | ||
176 | 546 | def findByProject(project, visible_by_user=None): | 561 | def findByProject(project, visible_by_user=None): |
177 | @@ -548,7 +563,7 @@ | |||
178 | 548 | 563 | ||
179 | 549 | :param project: An `IProduct`. | 564 | :param project: An `IProduct`. |
180 | 550 | :param visible_by_user: If not None, only return packages visible by | 565 | :param visible_by_user: If not None, only return packages visible by |
182 | 551 | this user. | 566 | this user; otherwise, only return publicly-visible packages. |
183 | 552 | """ | 567 | """ |
184 | 553 | 568 | ||
185 | 554 | def findByBranch(branch): | 569 | def findByBranch(branch): |
186 | @@ -571,12 +586,29 @@ | |||
187 | 571 | :param context: An `IPerson`, `IProduct, `IBranch`, | 586 | :param context: An `IPerson`, `IProduct, `IBranch`, |
188 | 572 | `IGitRepository`, or `IGitRef`. | 587 | `IGitRepository`, or `IGitRef`. |
189 | 573 | :param visible_by_user: If not None, only return packages visible by | 588 | :param visible_by_user: If not None, only return packages visible by |
191 | 574 | this user. | 589 | this user; otherwise, only return publicly-visible packages. |
192 | 575 | :param order_by_date: If True, order packages by descending | 590 | :param order_by_date: If True, order packages by descending |
193 | 576 | modification date. | 591 | modification date. |
194 | 577 | :raises BadSnapSearchContext: if the context is not understood. | 592 | :raises BadSnapSearchContext: if the context is not understood. |
195 | 578 | """ | 593 | """ |
196 | 579 | 594 | ||
197 | 595 | @operation_parameters(url=TextLine(title=_("The URL to search for."))) | ||
198 | 596 | @call_with(visible_by_user=REQUEST_USER) | ||
199 | 597 | @operation_returns_collection_of(ISnap) | ||
200 | 598 | @export_read_operation() | ||
201 | 599 | @operation_for_version("devel") | ||
202 | 600 | def findByURL(url, visible_by_user=None): | ||
203 | 601 | """Return all snap packages that build from the given URL. | ||
204 | 602 | |||
205 | 603 | This currently only works for packages that build directly from a | ||
206 | 604 | URL, rather than being linked to a Bazaar branch or Git repository | ||
207 | 605 | hosted in Launchpad. | ||
208 | 606 | |||
209 | 607 | :param url: A URL. | ||
210 | 608 | :param visible_by_user: If not None, only return packages visible by | ||
211 | 609 | this user; otherwise, only return publicly-visible packages. | ||
212 | 610 | """ | ||
213 | 611 | |||
214 | 580 | def preloadDataForSnaps(snaps, user): | 612 | def preloadDataForSnaps(snaps, user): |
215 | 581 | """Load the data related to a list of snap packages.""" | 613 | """Load the data related to a list of snap packages.""" |
216 | 582 | 614 | ||
217 | 583 | 615 | ||
218 | === modified file 'lib/lp/snappy/model/snap.py' | |||
219 | --- lib/lp/snappy/model/snap.py 2016-11-22 02:13:11 +0000 | |||
220 | +++ lib/lp/snappy/model/snap.py 2016-12-05 19:02:57 +0000 | |||
221 | @@ -17,6 +17,7 @@ | |||
222 | 17 | Desc, | 17 | Desc, |
223 | 18 | LeftJoin, | 18 | LeftJoin, |
224 | 19 | Not, | 19 | Not, |
225 | 20 | Or, | ||
226 | 20 | ) | 21 | ) |
227 | 21 | from storm.locals import ( | 22 | from storm.locals import ( |
228 | 22 | Bool, | 23 | Bool, |
229 | @@ -54,7 +55,10 @@ | |||
230 | 54 | IAllGitRepositories, | 55 | IAllGitRepositories, |
231 | 55 | IGitCollection, | 56 | IGitCollection, |
232 | 56 | ) | 57 | ) |
234 | 57 | from lp.code.interfaces.gitref import IGitRef | 58 | from lp.code.interfaces.gitref import ( |
235 | 59 | IGitRef, | ||
236 | 60 | IGitRefRemoteSet, | ||
237 | 61 | ) | ||
238 | 58 | from lp.code.interfaces.gitrepository import IGitRepository | 62 | from lp.code.interfaces.gitrepository import IGitRepository |
239 | 59 | from lp.code.model.branch import Branch | 63 | from lp.code.model.branch import Branch |
240 | 60 | from lp.code.model.branchcollection import GenericBranchCollection | 64 | from lp.code.model.branchcollection import GenericBranchCollection |
241 | @@ -71,6 +75,7 @@ | |||
242 | 71 | IHasOwner, | 75 | IHasOwner, |
243 | 72 | IPersonRoles, | 76 | IPersonRoles, |
244 | 73 | ) | 77 | ) |
245 | 78 | from lp.registry.model.teammembership import TeamParticipation | ||
246 | 74 | from lp.services.config import config | 79 | from lp.services.config import config |
247 | 75 | from lp.services.database.bulk import load_related | 80 | from lp.services.database.bulk import load_related |
248 | 76 | from lp.services.database.constants import ( | 81 | from lp.services.database.constants import ( |
249 | @@ -115,6 +120,7 @@ | |||
250 | 115 | from lp.snappy.interfaces.snappyseries import ISnappyDistroSeriesSet | 120 | from lp.snappy.interfaces.snappyseries import ISnappyDistroSeriesSet |
251 | 116 | from lp.snappy.model.snapbuild import SnapBuild | 121 | from lp.snappy.model.snapbuild import SnapBuild |
252 | 117 | from lp.soyuz.interfaces.archive import ArchiveDisabled | 122 | from lp.soyuz.interfaces.archive import ArchiveDisabled |
253 | 123 | from lp.soyuz.interfaces.buildrecords import IncompatibleArguments | ||
254 | 118 | from lp.soyuz.model.archive import ( | 124 | from lp.soyuz.model.archive import ( |
255 | 119 | Archive, | 125 | Archive, |
256 | 120 | get_enabled_archive_filter, | 126 | get_enabled_archive_filter, |
257 | @@ -163,6 +169,8 @@ | |||
258 | 163 | git_repository_id = Int(name='git_repository', allow_none=True) | 169 | git_repository_id = Int(name='git_repository', allow_none=True) |
259 | 164 | git_repository = Reference(git_repository_id, 'GitRepository.id') | 170 | git_repository = Reference(git_repository_id, 'GitRepository.id') |
260 | 165 | 171 | ||
261 | 172 | git_repository_url = Unicode(name='git_repository_url', allow_none=True) | ||
262 | 173 | |||
263 | 166 | git_path = Unicode(name='git_path', allow_none=True) | 174 | git_path = Unicode(name='git_path', allow_none=True) |
264 | 167 | 175 | ||
265 | 168 | auto_build = Bool(name='auto_build', allow_none=False) | 176 | auto_build = Bool(name='auto_build', allow_none=False) |
266 | @@ -226,6 +234,9 @@ | |||
267 | 226 | """See `ISnap`.""" | 234 | """See `ISnap`.""" |
268 | 227 | if self.git_repository is not None: | 235 | if self.git_repository is not None: |
269 | 228 | return self.git_repository.getRefByPath(self.git_path) | 236 | return self.git_repository.getRefByPath(self.git_path) |
270 | 237 | elif self.git_repository_url is not None: | ||
271 | 238 | return getUtility(IGitRefRemoteSet).new( | ||
272 | 239 | self.git_repository_url, self.git_path) | ||
273 | 229 | else: | 240 | else: |
274 | 230 | return None | 241 | return None |
275 | 231 | 242 | ||
276 | @@ -234,9 +245,11 @@ | |||
277 | 234 | """See `ISnap`.""" | 245 | """See `ISnap`.""" |
278 | 235 | if value is not None: | 246 | if value is not None: |
279 | 236 | self.git_repository = value.repository | 247 | self.git_repository = value.repository |
280 | 248 | self.git_repository_url = value.repository_url | ||
281 | 237 | self.git_path = value.path | 249 | self.git_path = value.path |
282 | 238 | else: | 250 | else: |
283 | 239 | self.git_repository = None | 251 | self.git_repository = None |
284 | 252 | self.git_repository_url = None | ||
285 | 240 | self.git_path = None | 253 | self.git_path = None |
286 | 241 | 254 | ||
287 | 242 | @property | 255 | @property |
288 | @@ -549,7 +562,8 @@ | |||
289 | 549 | """See `ISnapSet`.""" | 562 | """See `ISnapSet`.""" |
290 | 550 | 563 | ||
291 | 551 | def new(self, registrant, owner, distro_series, name, description=None, | 564 | def new(self, registrant, owner, distro_series, name, description=None, |
293 | 552 | branch=None, git_ref=None, auto_build=False, | 565 | branch=None, git_repository=None, git_repository_url=None, |
294 | 566 | git_path=None, git_ref=None, auto_build=False, | ||
295 | 553 | auto_build_archive=None, auto_build_pocket=None, | 567 | auto_build_archive=None, auto_build_pocket=None, |
296 | 554 | require_virtualized=True, processors=None, date_created=DEFAULT, | 568 | require_virtualized=True, processors=None, date_created=DEFAULT, |
297 | 555 | private=False, store_upload=False, store_series=None, | 569 | private=False, store_upload=False, store_series=None, |
298 | @@ -565,6 +579,21 @@ | |||
299 | 565 | "%s cannot create snap packages owned by %s." % | 579 | "%s cannot create snap packages owned by %s." % |
300 | 566 | (registrant.displayname, owner.displayname)) | 580 | (registrant.displayname, owner.displayname)) |
301 | 567 | 581 | ||
302 | 582 | if sum([git_repository is not None, git_repository_url is not None, | ||
303 | 583 | git_ref is not None]) > 1: | ||
304 | 584 | raise IncompatibleArguments( | ||
305 | 585 | "You cannot specify more than one of 'git_repository', " | ||
306 | 586 | "'git_repository_url', and 'git_ref'.") | ||
307 | 587 | if ((git_repository is None and git_repository_url is None) != | ||
308 | 588 | (git_path is None)): | ||
309 | 589 | raise IncompatibleArguments( | ||
310 | 590 | "You must specify both or neither of " | ||
311 | 591 | "'git_repository'/'git_repository_url' and 'git_path'.") | ||
312 | 592 | if git_repository is not None: | ||
313 | 593 | git_ref = git_repository.getRefByPath(git_path) | ||
314 | 594 | elif git_repository_url is not None: | ||
315 | 595 | git_ref = getUtility(IGitRefRemoteSet).new( | ||
316 | 596 | git_repository_url, git_path) | ||
317 | 568 | if branch is None and git_ref is None: | 597 | if branch is None and git_ref is None: |
318 | 569 | raise NoSourceForSnap | 598 | raise NoSourceForSnap |
319 | 570 | if self.exists(owner, name): | 599 | if self.exists(owner, name): |
320 | @@ -603,8 +632,8 @@ | |||
321 | 603 | return True | 632 | return True |
322 | 604 | 633 | ||
323 | 605 | # Public snaps with private sources are not allowed. | 634 | # Public snaps with private sources are not allowed. |
326 | 606 | source_ref = branch or git_ref | 635 | source = branch or git_ref |
327 | 607 | if source_ref.information_type in PRIVATE_INFORMATION_TYPES: | 636 | if source.information_type in PRIVATE_INFORMATION_TYPES: |
328 | 608 | return False | 637 | return False |
329 | 609 | 638 | ||
330 | 610 | # Public snaps owned by private teams are not allowed. | 639 | # Public snaps owned by private teams are not allowed. |
331 | @@ -653,8 +682,12 @@ | |||
332 | 653 | return owned.union(packaged) | 682 | return owned.union(packaged) |
333 | 654 | 683 | ||
334 | 655 | bzr_collection = removeSecurityProxy(getUtility(IAllBranches)) | 684 | bzr_collection = removeSecurityProxy(getUtility(IAllBranches)) |
335 | 685 | bzr_snaps = _getSnaps(bzr_collection) | ||
336 | 656 | git_collection = removeSecurityProxy(getUtility(IAllGitRepositories)) | 686 | git_collection = removeSecurityProxy(getUtility(IAllGitRepositories)) |
338 | 657 | return _getSnaps(bzr_collection).union(_getSnaps(git_collection)) | 687 | git_snaps = _getSnaps(git_collection) |
339 | 688 | git_url_snaps = IStore(Snap).find( | ||
340 | 689 | Snap, Snap.owner == person, Snap.git_repository_url != None) | ||
341 | 690 | return bzr_snaps.union(git_snaps).union(git_url_snaps) | ||
342 | 658 | 691 | ||
343 | 659 | def findByProject(self, project, visible_by_user=None): | 692 | def findByProject(self, project, visible_by_user=None): |
344 | 660 | """See `ISnapSet`.""" | 693 | """See `ISnapSet`.""" |
345 | @@ -705,6 +738,28 @@ | |||
346 | 705 | snaps.order_by(Desc(Snap.date_last_modified)) | 738 | snaps.order_by(Desc(Snap.date_last_modified)) |
347 | 706 | return snaps | 739 | return snaps |
348 | 707 | 740 | ||
349 | 741 | def findByURL(self, url, visible_by_user=None): | ||
350 | 742 | """See `ISnapSet`.""" | ||
351 | 743 | clauses = [Snap.git_repository_url == url] | ||
352 | 744 | # XXX cjwatson 2016-11-25: This is in principle a poor query, but we | ||
353 | 745 | # don't yet have the access grant infrastructure to do better, and | ||
354 | 746 | # in any case since we're querying for a single URL the numbers | ||
355 | 747 | # involved should be very small. | ||
356 | 748 | if visible_by_user is None: | ||
357 | 749 | visibility_clause = Snap.private == False | ||
358 | 750 | else: | ||
359 | 751 | roles = IPersonRoles(visible_by_user) | ||
360 | 752 | if roles.in_admin or roles.in_commercial_admin: | ||
361 | 753 | visibility_clause = True | ||
362 | 754 | else: | ||
363 | 755 | visibility_clause = Or( | ||
364 | 756 | Snap.private == False, | ||
365 | 757 | And( | ||
366 | 758 | TeamParticipation.person == visible_by_user, | ||
367 | 759 | TeamParticipation.teamID == Snap.owner_id)) | ||
368 | 760 | clauses.append(visibility_clause) | ||
369 | 761 | return IStore(Snap).find(Snap, *clauses).config(distinct=True) | ||
370 | 762 | |||
371 | 708 | def preloadDataForSnaps(self, snaps, user=None): | 763 | def preloadDataForSnaps(self, snaps, user=None): |
372 | 709 | """See `ISnapSet`.""" | 764 | """See `ISnapSet`.""" |
373 | 710 | snaps = [removeSecurityProxy(snap) for snap in snaps] | 765 | snaps = [removeSecurityProxy(snap) for snap in snaps] |
374 | 711 | 766 | ||
375 | === modified file 'lib/lp/snappy/model/snapbuildbehaviour.py' | |||
376 | --- lib/lp/snappy/model/snapbuildbehaviour.py 2016-09-21 02:50:41 +0000 | |||
377 | +++ lib/lp/snappy/model/snapbuildbehaviour.py 2016-12-05 19:02:57 +0000 | |||
378 | @@ -1,4 +1,4 @@ | |||
380 | 1 | # Copyright 2015 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2015-2016 Canonical Ltd. This software is licensed under the |
381 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
382 | 3 | 3 | ||
383 | 4 | """An `IBuildFarmJobBehaviour` for `SnapBuild`. | 4 | """An `IBuildFarmJobBehaviour` for `SnapBuild`. |
384 | @@ -105,7 +105,11 @@ | |||
385 | 105 | if build.snap.branch is not None: | 105 | if build.snap.branch is not None: |
386 | 106 | args["branch"] = build.snap.branch.bzr_identity | 106 | args["branch"] = build.snap.branch.bzr_identity |
387 | 107 | elif build.snap.git_ref is not None: | 107 | elif build.snap.git_ref is not None: |
389 | 108 | args["git_repository"] = build.snap.git_repository.git_https_url | 108 | if build.snap.git_ref.repository_url is not None: |
390 | 109 | args["git_repository"] = build.snap.git_ref.repository_url | ||
391 | 110 | else: | ||
392 | 111 | args["git_repository"] = ( | ||
393 | 112 | build.snap.git_repository.git_https_url) | ||
394 | 109 | # "git clone -b" doesn't accept full ref names. If this becomes | 113 | # "git clone -b" doesn't accept full ref names. If this becomes |
395 | 110 | # a problem then we could change launchpad-buildd to do "git | 114 | # a problem then we could change launchpad-buildd to do "git |
396 | 111 | # clone" followed by "git checkout" instead. | 115 | # clone" followed by "git checkout" instead. |
397 | 112 | 116 | ||
398 | === modified file 'lib/lp/snappy/tests/test_snap.py' | |||
399 | --- lib/lp/snappy/tests/test_snap.py 2016-11-22 02:13:11 +0000 | |||
400 | +++ lib/lp/snappy/tests/test_snap.py 2016-12-05 19:02:57 +0000 | |||
401 | @@ -87,7 +87,10 @@ | |||
402 | 87 | DoesNotSnapshot, | 87 | DoesNotSnapshot, |
403 | 88 | HasQueryCount, | 88 | HasQueryCount, |
404 | 89 | ) | 89 | ) |
406 | 90 | from lp.testing.pages import webservice_for_person | 90 | from lp.testing.pages import ( |
407 | 91 | LaunchpadWebServiceCaller, | ||
408 | 92 | webservice_for_person, | ||
409 | 93 | ) | ||
410 | 91 | 94 | ||
411 | 92 | 95 | ||
412 | 93 | class TestSnapFeatureFlag(TestCaseWithFactory): | 96 | class TestSnapFeatureFlag(TestCaseWithFactory): |
413 | @@ -591,6 +594,18 @@ | |||
414 | 591 | self.assertTrue(snap.require_virtualized) | 594 | self.assertTrue(snap.require_virtualized) |
415 | 592 | self.assertFalse(snap.private) | 595 | self.assertFalse(snap.private) |
416 | 593 | 596 | ||
417 | 597 | def test_creation_git_url(self): | ||
418 | 598 | # A Snap can be backed directly by a URL for an external Git | ||
419 | 599 | # repository, rather than a Git repository hosted in Launchpad. | ||
420 | 600 | ref = self.factory.makeGitRefRemote() | ||
421 | 601 | components = self.makeSnapComponents(git_ref=ref) | ||
422 | 602 | snap = getUtility(ISnapSet).new(**components) | ||
423 | 603 | self.assertIsNone(snap.branch) | ||
424 | 604 | self.assertIsNone(snap.git_repository) | ||
425 | 605 | self.assertEqual(ref.repository_url, snap.git_repository_url) | ||
426 | 606 | self.assertEqual(ref.path, snap.git_path) | ||
427 | 607 | self.assertEqual(ref, snap.git_ref) | ||
428 | 608 | |||
429 | 594 | def test_private_snap_for_public_sources(self): | 609 | def test_private_snap_for_public_sources(self): |
430 | 595 | # Creating private snaps for public sources is allowed. | 610 | # Creating private snaps for public sources is allowed. |
431 | 596 | [ref] = self.factory.makeGitRefs() | 611 | [ref] = self.factory.makeGitRefs() |
432 | @@ -803,6 +818,20 @@ | |||
433 | 803 | BadSnapSearchContext, snap_set.findByContext, | 818 | BadSnapSearchContext, snap_set.findByContext, |
434 | 804 | self.factory.makeDistribution()) | 819 | self.factory.makeDistribution()) |
435 | 805 | 820 | ||
436 | 821 | def test_findByURL(self): | ||
437 | 822 | # ISnapSet.findByURL returns visible Snaps with the given URL. | ||
438 | 823 | urls = [u"https://git.example.org/foo", u"https://git.example.org/bar"] | ||
439 | 824 | snaps = [] | ||
440 | 825 | for url in urls: | ||
441 | 826 | snaps.append(self.factory.makeSnap( | ||
442 | 827 | git_ref=self.factory.makeGitRefRemote(repository_url=url))) | ||
443 | 828 | snaps.append( | ||
444 | 829 | self.factory.makeSnap(branch=self.factory.makeAnyBranch())) | ||
445 | 830 | snaps.append( | ||
446 | 831 | self.factory.makeSnap(git_ref=self.factory.makeGitRefs()[0])) | ||
447 | 832 | self.assertContentEqual( | ||
448 | 833 | [snaps[0]], getUtility(ISnapSet).findByURL(urls[0])) | ||
449 | 834 | |||
450 | 806 | def test__findStaleSnaps(self): | 835 | def test__findStaleSnaps(self): |
451 | 807 | # Stale; not built automatically. | 836 | # Stale; not built automatically. |
452 | 808 | self.factory.makeSnap(is_stale=True) | 837 | self.factory.makeSnap(is_stale=True) |
453 | @@ -1252,6 +1281,52 @@ | |||
454 | 1252 | "No such snap package with this owner: 'nonexistent'.", | 1281 | "No such snap package with this owner: 'nonexistent'.", |
455 | 1253 | response.body) | 1282 | response.body) |
456 | 1254 | 1283 | ||
457 | 1284 | def test_findByURL(self): | ||
458 | 1285 | # lp.snaps.findByURL returns visible Snaps with the given URL. | ||
459 | 1286 | persons = [self.factory.makePerson(), self.factory.makePerson()] | ||
460 | 1287 | urls = [u"https://git.example.org/foo", u"https://git.example.org/bar"] | ||
461 | 1288 | snaps = [] | ||
462 | 1289 | for url in urls: | ||
463 | 1290 | for person in persons: | ||
464 | 1291 | for private in (False, True): | ||
465 | 1292 | ref = self.factory.makeGitRefRemote(repository_url=url) | ||
466 | 1293 | snaps.append(self.factory.makeSnap( | ||
467 | 1294 | registrant=person, git_ref=ref, private=private)) | ||
468 | 1295 | with admin_logged_in(): | ||
469 | 1296 | ws_snaps = [ | ||
470 | 1297 | self.webservice.getAbsoluteUrl(api_url(snap)) | ||
471 | 1298 | for snap in snaps] | ||
472 | 1299 | commercial_admin = ( | ||
473 | 1300 | getUtility(ILaunchpadCelebrities).commercial_admin.teamowner) | ||
474 | 1301 | logout() | ||
475 | 1302 | # Anonymous requests can only see public snaps. | ||
476 | 1303 | anon_webservice = LaunchpadWebServiceCaller("test", "") | ||
477 | 1304 | response = anon_webservice.named_get( | ||
478 | 1305 | "/+snaps", "findByURL", url=urls[0], api_version="devel") | ||
479 | 1306 | self.assertEqual(200, response.status) | ||
480 | 1307 | self.assertContentEqual( | ||
481 | 1308 | [ws_snaps[0], ws_snaps[2]], | ||
482 | 1309 | [entry["self_link"] for entry in response.jsonBody()["entries"]]) | ||
483 | 1310 | # persons[0] can see both public snaps with this URL, as well as | ||
484 | 1311 | # their own private snap. | ||
485 | 1312 | webservice = webservice_for_person( | ||
486 | 1313 | persons[0], permission=OAuthPermission.READ_PRIVATE) | ||
487 | 1314 | response = webservice.named_get( | ||
488 | 1315 | "/+snaps", "findByURL", url=urls[0], api_version="devel") | ||
489 | 1316 | self.assertEqual(200, response.status) | ||
490 | 1317 | self.assertContentEqual( | ||
491 | 1318 | ws_snaps[:3], | ||
492 | 1319 | [entry["self_link"] for entry in response.jsonBody()["entries"]]) | ||
493 | 1320 | # Admins can see all snaps with this URL. | ||
494 | 1321 | commercial_admin_webservice = webservice_for_person( | ||
495 | 1322 | commercial_admin, permission=OAuthPermission.READ_PRIVATE) | ||
496 | 1323 | response = commercial_admin_webservice.named_get( | ||
497 | 1324 | "/+snaps", "findByURL", url=urls[0], api_version="devel") | ||
498 | 1325 | self.assertEqual(200, response.status) | ||
499 | 1326 | self.assertContentEqual( | ||
500 | 1327 | ws_snaps[:4], | ||
501 | 1328 | [entry["self_link"] for entry in response.jsonBody()["entries"]]) | ||
502 | 1329 | |||
503 | 1255 | def setProcessors(self, user, snap, names): | 1330 | def setProcessors(self, user, snap, names): |
504 | 1256 | ws = webservice_for_person( | 1331 | ws = webservice_for_person( |
505 | 1257 | user, permission=OAuthPermission.WRITE_PUBLIC) | 1332 | user, permission=OAuthPermission.WRITE_PUBLIC) |
506 | 1258 | 1333 | ||
507 | === modified file 'lib/lp/snappy/tests/test_snapbuildbehaviour.py' | |||
508 | --- lib/lp/snappy/tests/test_snapbuildbehaviour.py 2016-06-28 21:10:18 +0000 | |||
509 | +++ lib/lp/snappy/tests/test_snapbuildbehaviour.py 2016-12-05 19:02:57 +0000 | |||
510 | @@ -11,13 +11,13 @@ | |||
511 | 11 | 11 | ||
512 | 12 | import fixtures | 12 | import fixtures |
513 | 13 | from mock import ( | 13 | from mock import ( |
514 | 14 | Mock, | ||
515 | 14 | patch, | 15 | patch, |
516 | 15 | Mock, | ||
517 | 16 | ) | 16 | ) |
518 | 17 | import transaction | ||
519 | 18 | from testtools import ExpectedException | 17 | from testtools import ExpectedException |
520 | 19 | from testtools.deferredruntest import AsynchronousDeferredRunTest | 18 | from testtools.deferredruntest import AsynchronousDeferredRunTest |
521 | 20 | from testtools.matchers import IsInstance | 19 | from testtools.matchers import IsInstance |
522 | 20 | import transaction | ||
523 | 21 | from twisted.internet import defer | 21 | from twisted.internet import defer |
524 | 22 | from twisted.trial.unittest import TestCase as TrialTestCase | 22 | from twisted.trial.unittest import TestCase as TrialTestCase |
525 | 23 | from zope.component import getUtility | 23 | from zope.component import getUtility |
526 | @@ -244,6 +244,28 @@ | |||
527 | 244 | }, args) | 244 | }, args) |
528 | 245 | 245 | ||
529 | 246 | @defer.inlineCallbacks | 246 | @defer.inlineCallbacks |
530 | 247 | def test_extraBuildArgs_git_url(self): | ||
531 | 248 | # _extraBuildArgs returns appropriate arguments if asked to build a | ||
532 | 249 | # job for a Git branch backed by a URL for an external repository. | ||
533 | 250 | url = u"https://git.example.org/foo" | ||
534 | 251 | ref = self.factory.makeGitRefRemote( | ||
535 | 252 | repository_url=url, path=u"refs/heads/master") | ||
536 | 253 | job = self.makeJob(git_ref=ref) | ||
537 | 254 | expected_archives = get_sources_list_for_building( | ||
538 | 255 | job.build, job.build.distro_arch_series, None) | ||
539 | 256 | args = yield job._extraBuildArgs() | ||
540 | 257 | self.assertEqual({ | ||
541 | 258 | "archive_private": False, | ||
542 | 259 | "archives": expected_archives, | ||
543 | 260 | "arch_tag": "i386", | ||
544 | 261 | "git_repository": url, | ||
545 | 262 | "git_path": "master", | ||
546 | 263 | "name": u"test-snap", | ||
547 | 264 | "proxy_url": self.proxy_url, | ||
548 | 265 | "revocation_endpoint": self.revocation_endpoint, | ||
549 | 266 | }, args) | ||
550 | 267 | |||
551 | 268 | @defer.inlineCallbacks | ||
552 | 247 | def test_extraBuildArgs_proxy_url_set(self): | 269 | def test_extraBuildArgs_proxy_url_set(self): |
553 | 248 | job = self.makeJob() | 270 | job = self.makeJob() |
554 | 249 | build_request = yield job.composeBuildRequest(None) | 271 | build_request = yield job.composeBuildRequest(None) |
555 | 250 | 272 | ||
556 | === modified file 'lib/lp/testing/matchers.py' | |||
557 | --- lib/lp/testing/matchers.py 2015-10-13 16:58:20 +0000 | |||
558 | +++ lib/lp/testing/matchers.py 2016-12-05 19:02:57 +0000 | |||
559 | @@ -1,4 +1,4 @@ | |||
561 | 1 | # Copyright 2010 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2010-2016 Canonical Ltd. This software is licensed under the |
562 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
563 | 3 | 3 | ||
564 | 4 | __metaclass__ = type | 4 | __metaclass__ = type |
565 | @@ -395,7 +395,8 @@ | |||
566 | 395 | self.soup_content = soup_content | 395 | self.soup_content = soup_content |
567 | 396 | 396 | ||
568 | 397 | def get_details(self): | 397 | def get_details(self): |
570 | 398 | return {'content': self.soup_content} | 398 | content = unicode(self.soup_content).encode('utf8') |
571 | 399 | return {'content': Content(UTF8_TEXT, lambda: [content])} | ||
572 | 399 | 400 | ||
573 | 400 | 401 | ||
574 | 401 | class MissingElement(SoupMismatch): | 402 | class MissingElement(SoupMismatch): |