Merge lp:~cjwatson/launchpad/git-path-HEAD into lp:launchpad
- git-path-HEAD
- Merge into devel
Proposed by
Colin Watson
Status: | Merged |
---|---|
Merged at revision: | 18384 |
Proposed branch: | lp:~cjwatson/launchpad/git-path-HEAD |
Merge into: | lp:launchpad |
Diff against target: |
300 lines (+143/-29) 6 files modified
lib/lp/code/configure.zcml (+6/-1) lib/lp/code/model/gitref.py (+71/-25) lib/lp/code/model/gitrepository.py (+6/-1) lib/lp/code/model/tests/test_gitrepository.py (+16/-0) lib/lp/snappy/model/snapbuildbehaviour.py (+3/-2) lib/lp/snappy/tests/test_snapbuildbehaviour.py (+41/-0) |
To merge this branch: | bzr merge lp:~cjwatson/launchpad/git-path-HEAD |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant (community) | code | Approve | |
Review via email: mp+323660@code.launchpad.net |
Commit message
Allow configuring a snap to build from the current branch of a Git repository rather than explicitly naming a branch.
Description of the change
This is a better default for build.snapcraft.io than hardcoding master.
This must not be landed until https:/
To post a comment you must log in.
Revision history for this message
William Grant (wgrant) : | # |
review:
Approve
(code)
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lib/lp/code/configure.zcml' | |||
2 | --- lib/lp/code/configure.zcml 2016-12-02 12:04:11 +0000 | |||
3 | +++ lib/lp/code/configure.zcml 2017-05-05 12:17:30 +0000 | |||
4 | @@ -1,4 +1,4 @@ | |||
6 | 1 | <!-- Copyright 2009-2016 Canonical Ltd. This software is licensed under the | 1 | <!-- Copyright 2009-2017 Canonical Ltd. This software is licensed under the |
7 | 2 | GNU Affero General Public License version 3 (see the file LICENSE). | 2 | GNU Affero General Public License version 3 (see the file LICENSE). |
8 | 3 | --> | 3 | --> |
9 | 4 | 4 | ||
10 | @@ -896,6 +896,11 @@ | |||
11 | 896 | permission="launchpad.View" | 896 | permission="launchpad.View" |
12 | 897 | interface="lp.code.interfaces.gitref.IGitRef" /> | 897 | interface="lp.code.interfaces.gitref.IGitRef" /> |
13 | 898 | </class> | 898 | </class> |
14 | 899 | <class class="lp.code.model.gitref.GitRefDefault"> | ||
15 | 900 | <require | ||
16 | 901 | permission="launchpad.View" | ||
17 | 902 | interface="lp.code.interfaces.gitref.IGitRef" /> | ||
18 | 903 | </class> | ||
19 | 899 | <class class="lp.code.model.gitref.GitRefFrozen"> | 904 | <class class="lp.code.model.gitref.GitRefFrozen"> |
20 | 900 | <require | 905 | <require |
21 | 901 | permission="launchpad.View" | 906 | permission="launchpad.View" |
22 | 902 | 907 | ||
23 | === modified file 'lib/lp/code/model/gitref.py' | |||
24 | --- lib/lp/code/model/gitref.py 2016-12-05 14:46:40 +0000 | |||
25 | +++ lib/lp/code/model/gitref.py 2017-05-05 12:17:30 +0000 | |||
26 | @@ -1,4 +1,4 @@ | |||
28 | 1 | # Copyright 2015-2016 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2015-2017 Canonical Ltd. This software is licensed under the |
29 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
30 | 3 | 3 | ||
31 | 4 | __metaclass__ = type | 4 | __metaclass__ = type |
32 | @@ -536,8 +536,72 @@ | |||
33 | 536 | commit_message=commit_message, review_requests=review_requests) | 536 | commit_message=commit_message, review_requests=review_requests) |
34 | 537 | 537 | ||
35 | 538 | 538 | ||
38 | 539 | @implementer(IGitRef) | 539 | class GitRefDatabaseBackedMixin(GitRefMixin): |
39 | 540 | class GitRefFrozen(GitRefMixin): | 540 | """A mixin for virtual Git references backed by a database record.""" |
40 | 541 | |||
41 | 542 | @property | ||
42 | 543 | def _non_database_attributes(self): | ||
43 | 544 | """A sequence of attributes not backed by the database.""" | ||
44 | 545 | raise NotImplementedError() | ||
45 | 546 | |||
46 | 547 | @property | ||
47 | 548 | def _self_in_database(self): | ||
48 | 549 | """Return the equivalent database-backed record of self.""" | ||
49 | 550 | raise NotImplementedError() | ||
50 | 551 | |||
51 | 552 | def __getattr__(self, name): | ||
52 | 553 | return getattr(self._self_in_database, name) | ||
53 | 554 | |||
54 | 555 | def __setattr__(self, name, value): | ||
55 | 556 | if name in self._non_database_attributes: | ||
56 | 557 | self.__dict__[name] = value | ||
57 | 558 | else: | ||
58 | 559 | setattr(self._self_in_database, name, value) | ||
59 | 560 | |||
60 | 561 | def __eq__(self, other): | ||
61 | 562 | return ( | ||
62 | 563 | self.repository == other.repository and | ||
63 | 564 | self.path == other.path and | ||
64 | 565 | self.commit_sha1 == other.commit_sha1) | ||
65 | 566 | |||
66 | 567 | def __ne__(self, other): | ||
67 | 568 | return not self == other | ||
68 | 569 | |||
69 | 570 | def __hash__(self): | ||
70 | 571 | return hash(self.repository) ^ hash(self.path) ^ hash(self.commit_sha1) | ||
71 | 572 | |||
72 | 573 | |||
73 | 574 | @implementer(IGitRef) | ||
74 | 575 | class GitRefDefault(GitRefDatabaseBackedMixin): | ||
75 | 576 | """A reference to the default branch in a Git repository. | ||
76 | 577 | |||
77 | 578 | This always refers to whatever the default branch currently is, even if | ||
78 | 579 | it changes later. | ||
79 | 580 | """ | ||
80 | 581 | |||
81 | 582 | def __init__(self, repository): | ||
82 | 583 | self.repository_id = repository.id | ||
83 | 584 | self.repository = repository | ||
84 | 585 | self.path = u"HEAD" | ||
85 | 586 | |||
86 | 587 | _non_database_attributes = ("repository_id", "repository", "path") | ||
87 | 588 | |||
88 | 589 | @property | ||
89 | 590 | def _self_in_database(self): | ||
90 | 591 | """See `GitRefDatabaseBackedMixin`.""" | ||
91 | 592 | path = self.repository.default_branch | ||
92 | 593 | if path is None: | ||
93 | 594 | raise NotFoundError("Repository '%s' has no default branch") | ||
94 | 595 | ref = IStore(GitRef).get(GitRef, (self.repository_id, path)) | ||
95 | 596 | if ref is None: | ||
96 | 597 | raise NotFoundError( | ||
97 | 598 | "Repository '%s' has default branch '%s', but there is no " | ||
98 | 599 | "such reference" % (self.repository, path)) | ||
99 | 600 | return ref | ||
100 | 601 | |||
101 | 602 | |||
102 | 603 | @implementer(IGitRef) | ||
103 | 604 | class GitRefFrozen(GitRefDatabaseBackedMixin): | ||
104 | 541 | """A frozen Git reference. | 605 | """A frozen Git reference. |
105 | 542 | 606 | ||
106 | 543 | This is like a GitRef, but is frozen at a particular commit, even if the | 607 | This is like a GitRef, but is frozen at a particular commit, even if the |
107 | @@ -554,9 +618,12 @@ | |||
108 | 554 | self.path = path | 618 | self.path = path |
109 | 555 | self.commit_sha1 = commit_sha1 | 619 | self.commit_sha1 = commit_sha1 |
110 | 556 | 620 | ||
111 | 621 | _non_database_attributes = ( | ||
112 | 622 | "repository_id", "repository", "path", "commit_sha1") | ||
113 | 623 | |||
114 | 557 | @property | 624 | @property |
115 | 558 | def _self_in_database(self): | 625 | def _self_in_database(self): |
117 | 559 | """Return the equivalent database-backed record of self.""" | 626 | """See `GitRefDatabaseBackedMixin`.""" |
118 | 560 | ref = IStore(GitRef).get(GitRef, (self.repository_id, self.path)) | 627 | ref = IStore(GitRef).get(GitRef, (self.repository_id, self.path)) |
119 | 561 | if ref is None: | 628 | if ref is None: |
120 | 562 | raise NotFoundError( | 629 | raise NotFoundError( |
121 | @@ -564,27 +631,6 @@ | |||
122 | 564 | "'%s'" % (self.repository, self.path)) | 631 | "'%s'" % (self.repository, self.path)) |
123 | 565 | return ref | 632 | return ref |
124 | 566 | 633 | ||
125 | 567 | def __getattr__(self, name): | ||
126 | 568 | return getattr(self._self_in_database, name) | ||
127 | 569 | |||
128 | 570 | def __setattr__(self, name, value): | ||
129 | 571 | if name in ("repository_id", "repository", "path", "commit_sha1"): | ||
130 | 572 | self.__dict__[name] = value | ||
131 | 573 | else: | ||
132 | 574 | setattr(self._self_in_database, name, value) | ||
133 | 575 | |||
134 | 576 | def __eq__(self, other): | ||
135 | 577 | return ( | ||
136 | 578 | self.repository == other.repository and | ||
137 | 579 | self.path == other.path and | ||
138 | 580 | self.commit_sha1 == other.commit_sha1) | ||
139 | 581 | |||
140 | 582 | def __ne__(self, other): | ||
141 | 583 | return not self == other | ||
142 | 584 | |||
143 | 585 | def __hash__(self): | ||
144 | 586 | return hash(self.repository) ^ hash(self.path) ^ hash(self.commit_sha1) | ||
145 | 587 | |||
146 | 588 | 634 | ||
147 | 589 | @implementer(IGitRef) | 635 | @implementer(IGitRef) |
148 | 590 | @provider(IGitRefRemoteSet) | 636 | @provider(IGitRefRemoteSet) |
149 | 591 | 637 | ||
150 | === modified file 'lib/lp/code/model/gitrepository.py' | |||
151 | --- lib/lp/code/model/gitrepository.py 2017-02-10 12:52:07 +0000 | |||
152 | +++ lib/lp/code/model/gitrepository.py 2017-05-05 12:17:30 +0000 | |||
153 | @@ -107,7 +107,10 @@ | |||
154 | 107 | from lp.code.interfaces.revision import IRevisionSet | 107 | from lp.code.interfaces.revision import IRevisionSet |
155 | 108 | from lp.code.mail.branch import send_git_repository_modified_notifications | 108 | from lp.code.mail.branch import send_git_repository_modified_notifications |
156 | 109 | from lp.code.model.branchmergeproposal import BranchMergeProposal | 109 | from lp.code.model.branchmergeproposal import BranchMergeProposal |
158 | 110 | from lp.code.model.gitref import GitRef | 110 | from lp.code.model.gitref import ( |
159 | 111 | GitRef, | ||
160 | 112 | GitRefDefault, | ||
161 | 113 | ) | ||
162 | 111 | from lp.code.model.gitsubscription import GitSubscription | 114 | from lp.code.model.gitsubscription import GitSubscription |
163 | 112 | from lp.registry.enums import PersonVisibility | 115 | from lp.registry.enums import PersonVisibility |
164 | 113 | from lp.registry.errors import CannotChangeInformationType | 116 | from lp.registry.errors import CannotChangeInformationType |
165 | @@ -515,6 +518,8 @@ | |||
166 | 515 | self.getInternalPath(), default_branch=ref.path) | 518 | self.getInternalPath(), default_branch=ref.path) |
167 | 516 | 519 | ||
168 | 517 | def getRefByPath(self, path): | 520 | def getRefByPath(self, path): |
169 | 521 | if path == u"HEAD": | ||
170 | 522 | return GitRefDefault(self) | ||
171 | 518 | paths = [path] | 523 | paths = [path] |
172 | 519 | if not path.startswith(u"refs/heads/"): | 524 | if not path.startswith(u"refs/heads/"): |
173 | 520 | paths.append(u"refs/heads/%s" % path) | 525 | paths.append(u"refs/heads/%s" % path) |
174 | 521 | 526 | ||
175 | === modified file 'lib/lp/code/model/tests/test_gitrepository.py' | |||
176 | --- lib/lp/code/model/tests/test_gitrepository.py 2017-02-10 12:52:07 +0000 | |||
177 | +++ lib/lp/code/model/tests/test_gitrepository.py 2017-05-05 12:17:30 +0000 | |||
178 | @@ -1182,6 +1182,20 @@ | |||
179 | 1182 | self.assertEqual(ref, ref.repository.getRefByPath(u"master")) | 1182 | self.assertEqual(ref, ref.repository.getRefByPath(u"master")) |
180 | 1183 | self.assertIsNone(ref.repository.getRefByPath(u"other")) | 1183 | self.assertIsNone(ref.repository.getRefByPath(u"other")) |
181 | 1184 | 1184 | ||
182 | 1185 | def test_getRefByPath_HEAD(self): | ||
183 | 1186 | # The special ref path "HEAD" always refers to the current default | ||
184 | 1187 | # branch. | ||
185 | 1188 | [ref] = self.factory.makeGitRefs(paths=[u"refs/heads/master"]) | ||
186 | 1189 | ref_HEAD = ref.repository.getRefByPath(u"HEAD") | ||
187 | 1190 | self.assertEqual(ref.repository, ref_HEAD.repository) | ||
188 | 1191 | self.assertEqual(u"HEAD", ref_HEAD.path) | ||
189 | 1192 | self.assertRaises(NotFoundError, getattr, ref_HEAD, "commit_sha1") | ||
190 | 1193 | removeSecurityProxy(ref.repository)._default_branch = ( | ||
191 | 1194 | u"refs/heads/missing") | ||
192 | 1195 | self.assertRaises(NotFoundError, getattr, ref_HEAD, "commit_sha1") | ||
193 | 1196 | removeSecurityProxy(ref.repository)._default_branch = ref.path | ||
194 | 1197 | self.assertEqual(ref.commit_sha1, ref_HEAD.commit_sha1) | ||
195 | 1198 | |||
196 | 1185 | def test_planRefChanges(self): | 1199 | def test_planRefChanges(self): |
197 | 1186 | # planRefChanges copes with planning changes to refs in a repository | 1200 | # planRefChanges copes with planning changes to refs in a repository |
198 | 1187 | # where some refs have been created, some deleted, and some changed. | 1201 | # where some refs have been created, some deleted, and some changed. |
199 | @@ -2700,6 +2714,7 @@ | |||
200 | 2700 | repository_db = self.factory.makeGitRepository() | 2714 | repository_db = self.factory.makeGitRepository() |
201 | 2701 | ref_dbs = self.factory.makeGitRefs( | 2715 | ref_dbs = self.factory.makeGitRefs( |
202 | 2702 | repository=repository_db, paths=[u"refs/heads/a", u"refs/heads/b"]) | 2716 | repository=repository_db, paths=[u"refs/heads/a", u"refs/heads/b"]) |
203 | 2717 | removeSecurityProxy(repository_db)._default_branch = u"refs/heads/a" | ||
204 | 2703 | repository_url = api_url(repository_db) | 2718 | repository_url = api_url(repository_db) |
205 | 2704 | ref_urls = [api_url(ref_db) for ref_db in ref_dbs] | 2719 | ref_urls = [api_url(ref_db) for ref_db in ref_dbs] |
206 | 2705 | webservice = webservice_for_person( | 2720 | webservice = webservice_for_person( |
207 | @@ -2710,6 +2725,7 @@ | |||
208 | 2710 | ("refs/heads/a", ref_urls[0]), | 2725 | ("refs/heads/a", ref_urls[0]), |
209 | 2711 | ("b", ref_urls[1]), | 2726 | ("b", ref_urls[1]), |
210 | 2712 | ("refs/heads/b", ref_urls[1]), | 2727 | ("refs/heads/b", ref_urls[1]), |
211 | 2728 | ("HEAD", "%s/+ref/HEAD" % repository_url), | ||
212 | 2713 | ): | 2729 | ): |
213 | 2714 | response = webservice.named_get( | 2730 | response = webservice.named_get( |
214 | 2715 | repository_url, "getRefByPath", path=path) | 2731 | repository_url, "getRefByPath", path=path) |
215 | 2716 | 2732 | ||
216 | === modified file 'lib/lp/snappy/model/snapbuildbehaviour.py' | |||
217 | --- lib/lp/snappy/model/snapbuildbehaviour.py 2016-12-05 23:11:57 +0000 | |||
218 | +++ lib/lp/snappy/model/snapbuildbehaviour.py 2017-05-05 12:17:30 +0000 | |||
219 | @@ -1,4 +1,4 @@ | |||
221 | 1 | # Copyright 2015-2016 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2015-2017 Canonical Ltd. This software is licensed under the |
222 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
223 | 3 | 3 | ||
224 | 4 | """An `IBuildFarmJobBehaviour` for `SnapBuild`. | 4 | """An `IBuildFarmJobBehaviour` for `SnapBuild`. |
225 | @@ -114,7 +114,8 @@ | |||
226 | 114 | # "git clone -b" doesn't accept full ref names. If this becomes | 114 | # "git clone -b" doesn't accept full ref names. If this becomes |
227 | 115 | # a problem then we could change launchpad-buildd to do "git | 115 | # a problem then we could change launchpad-buildd to do "git |
228 | 116 | # clone" followed by "git checkout" instead. | 116 | # clone" followed by "git checkout" instead. |
230 | 117 | args["git_path"] = build.snap.git_ref.name | 117 | if build.snap.git_path != u"HEAD": |
231 | 118 | args["git_path"] = build.snap.git_ref.name | ||
232 | 118 | else: | 119 | else: |
233 | 119 | raise CannotBuild( | 120 | raise CannotBuild( |
234 | 120 | "Source branch/repository for ~%s/%s has been deleted." % | 121 | "Source branch/repository for ~%s/%s has been deleted." % |
235 | 121 | 122 | ||
236 | === modified file 'lib/lp/snappy/tests/test_snapbuildbehaviour.py' | |||
237 | --- lib/lp/snappy/tests/test_snapbuildbehaviour.py 2017-01-27 12:44:41 +0000 | |||
238 | +++ lib/lp/snappy/tests/test_snapbuildbehaviour.py 2017-05-05 12:17:30 +0000 | |||
239 | @@ -22,6 +22,7 @@ | |||
240 | 22 | from twisted.internet import defer | 22 | from twisted.internet import defer |
241 | 23 | from twisted.trial.unittest import TestCase as TrialTestCase | 23 | from twisted.trial.unittest import TestCase as TrialTestCase |
242 | 24 | from zope.component import getUtility | 24 | from zope.component import getUtility |
243 | 25 | from zope.security.proxy import removeSecurityProxy | ||
244 | 25 | 26 | ||
245 | 26 | from lp.buildmaster.enums import BuildStatus | 27 | from lp.buildmaster.enums import BuildStatus |
246 | 27 | from lp.buildmaster.interfaces.builder import CannotBuild | 28 | from lp.buildmaster.interfaces.builder import CannotBuild |
247 | @@ -245,6 +246,26 @@ | |||
248 | 245 | }, args) | 246 | }, args) |
249 | 246 | 247 | ||
250 | 247 | @defer.inlineCallbacks | 248 | @defer.inlineCallbacks |
251 | 249 | def test_extraBuildArgs_git_HEAD(self): | ||
252 | 250 | # _extraBuildArgs returns appropriate arguments if asked to build a | ||
253 | 251 | # job for the default branch in a Launchpad-hosted Git repository. | ||
254 | 252 | [ref] = self.factory.makeGitRefs() | ||
255 | 253 | removeSecurityProxy(ref.repository)._default_branch = ref.path | ||
256 | 254 | job = self.makeJob(git_ref=ref.repository.getRefByPath(u"HEAD")) | ||
257 | 255 | expected_archives = get_sources_list_for_building( | ||
258 | 256 | job.build, job.build.distro_arch_series, None) | ||
259 | 257 | args = yield job._extraBuildArgs() | ||
260 | 258 | self.assertEqual({ | ||
261 | 259 | "archive_private": False, | ||
262 | 260 | "archives": expected_archives, | ||
263 | 261 | "arch_tag": "i386", | ||
264 | 262 | "git_repository": ref.repository.git_https_url, | ||
265 | 263 | "name": u"test-snap", | ||
266 | 264 | "proxy_url": self.proxy_url, | ||
267 | 265 | "revocation_endpoint": self.revocation_endpoint, | ||
268 | 266 | }, args) | ||
269 | 267 | |||
270 | 268 | @defer.inlineCallbacks | ||
271 | 248 | def test_extraBuildArgs_git_url(self): | 269 | def test_extraBuildArgs_git_url(self): |
272 | 249 | # _extraBuildArgs returns appropriate arguments if asked to build a | 270 | # _extraBuildArgs returns appropriate arguments if asked to build a |
273 | 250 | # job for a Git branch backed by a URL for an external repository. | 271 | # job for a Git branch backed by a URL for an external repository. |
274 | @@ -267,6 +288,26 @@ | |||
275 | 267 | }, args) | 288 | }, args) |
276 | 268 | 289 | ||
277 | 269 | @defer.inlineCallbacks | 290 | @defer.inlineCallbacks |
278 | 291 | def test_extraBuildArgs_git_url_HEAD(self): | ||
279 | 292 | # _extraBuildArgs returns appropriate arguments if asked to build a | ||
280 | 293 | # job for the default branch in an external Git repository. | ||
281 | 294 | url = u"https://git.example.org/foo" | ||
282 | 295 | ref = self.factory.makeGitRefRemote(repository_url=url, path=u"HEAD") | ||
283 | 296 | job = self.makeJob(git_ref=ref) | ||
284 | 297 | expected_archives = get_sources_list_for_building( | ||
285 | 298 | job.build, job.build.distro_arch_series, None) | ||
286 | 299 | args = yield job._extraBuildArgs() | ||
287 | 300 | self.assertEqual({ | ||
288 | 301 | "archive_private": False, | ||
289 | 302 | "archives": expected_archives, | ||
290 | 303 | "arch_tag": "i386", | ||
291 | 304 | "git_repository": url, | ||
292 | 305 | "name": u"test-snap", | ||
293 | 306 | "proxy_url": self.proxy_url, | ||
294 | 307 | "revocation_endpoint": self.revocation_endpoint, | ||
295 | 308 | }, args) | ||
296 | 309 | |||
297 | 310 | @defer.inlineCallbacks | ||
298 | 270 | def test_extraBuildArgs_proxy_url_set(self): | 311 | def test_extraBuildArgs_proxy_url_set(self): |
299 | 271 | job = self.makeJob() | 312 | job = self.makeJob() |
300 | 272 | build_request = yield job.composeBuildRequest(None) | 313 | build_request = yield job.composeBuildRequest(None) |