Merge lp:~cjwatson/launchpad/git-path-HEAD into lp:launchpad

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
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://code.launchpad.net/~cjwatson/launchpad-buildd/snap-default-branch/+merge/323604 has been deployed to production.

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 @@
5-<!-- Copyright 2009-2016 Canonical Ltd. This software is licensed under the
6+<!-- Copyright 2009-2017 Canonical Ltd. This software is licensed under the
7 GNU Affero General Public License version 3 (see the file LICENSE).
8 -->
9
10@@ -896,6 +896,11 @@
11 permission="launchpad.View"
12 interface="lp.code.interfaces.gitref.IGitRef" />
13 </class>
14+ <class class="lp.code.model.gitref.GitRefDefault">
15+ <require
16+ permission="launchpad.View"
17+ interface="lp.code.interfaces.gitref.IGitRef" />
18+ </class>
19 <class class="lp.code.model.gitref.GitRefFrozen">
20 <require
21 permission="launchpad.View"
22
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 @@
27-# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
28+# Copyright 2015-2017 Canonical Ltd. This software is licensed under the
29 # GNU Affero General Public License version 3 (see the file LICENSE).
30
31 __metaclass__ = type
32@@ -536,8 +536,72 @@
33 commit_message=commit_message, review_requests=review_requests)
34
35
36-@implementer(IGitRef)
37-class GitRefFrozen(GitRefMixin):
38+class GitRefDatabaseBackedMixin(GitRefMixin):
39+ """A mixin for virtual Git references backed by a database record."""
40+
41+ @property
42+ def _non_database_attributes(self):
43+ """A sequence of attributes not backed by the database."""
44+ raise NotImplementedError()
45+
46+ @property
47+ def _self_in_database(self):
48+ """Return the equivalent database-backed record of self."""
49+ raise NotImplementedError()
50+
51+ def __getattr__(self, name):
52+ return getattr(self._self_in_database, name)
53+
54+ def __setattr__(self, name, value):
55+ if name in self._non_database_attributes:
56+ self.__dict__[name] = value
57+ else:
58+ setattr(self._self_in_database, name, value)
59+
60+ def __eq__(self, other):
61+ return (
62+ self.repository == other.repository and
63+ self.path == other.path and
64+ self.commit_sha1 == other.commit_sha1)
65+
66+ def __ne__(self, other):
67+ return not self == other
68+
69+ def __hash__(self):
70+ return hash(self.repository) ^ hash(self.path) ^ hash(self.commit_sha1)
71+
72+
73+@implementer(IGitRef)
74+class GitRefDefault(GitRefDatabaseBackedMixin):
75+ """A reference to the default branch in a Git repository.
76+
77+ This always refers to whatever the default branch currently is, even if
78+ it changes later.
79+ """
80+
81+ def __init__(self, repository):
82+ self.repository_id = repository.id
83+ self.repository = repository
84+ self.path = u"HEAD"
85+
86+ _non_database_attributes = ("repository_id", "repository", "path")
87+
88+ @property
89+ def _self_in_database(self):
90+ """See `GitRefDatabaseBackedMixin`."""
91+ path = self.repository.default_branch
92+ if path is None:
93+ raise NotFoundError("Repository '%s' has no default branch")
94+ ref = IStore(GitRef).get(GitRef, (self.repository_id, path))
95+ if ref is None:
96+ raise NotFoundError(
97+ "Repository '%s' has default branch '%s', but there is no "
98+ "such reference" % (self.repository, path))
99+ return ref
100+
101+
102+@implementer(IGitRef)
103+class GitRefFrozen(GitRefDatabaseBackedMixin):
104 """A frozen Git reference.
105
106 This is like a GitRef, but is frozen at a particular commit, even if the
107@@ -554,9 +618,12 @@
108 self.path = path
109 self.commit_sha1 = commit_sha1
110
111+ _non_database_attributes = (
112+ "repository_id", "repository", "path", "commit_sha1")
113+
114 @property
115 def _self_in_database(self):
116- """Return the equivalent database-backed record of self."""
117+ """See `GitRefDatabaseBackedMixin`."""
118 ref = IStore(GitRef).get(GitRef, (self.repository_id, self.path))
119 if ref is None:
120 raise NotFoundError(
121@@ -564,27 +631,6 @@
122 "'%s'" % (self.repository, self.path))
123 return ref
124
125- def __getattr__(self, name):
126- return getattr(self._self_in_database, name)
127-
128- def __setattr__(self, name, value):
129- if name in ("repository_id", "repository", "path", "commit_sha1"):
130- self.__dict__[name] = value
131- else:
132- setattr(self._self_in_database, name, value)
133-
134- def __eq__(self, other):
135- return (
136- self.repository == other.repository and
137- self.path == other.path and
138- self.commit_sha1 == other.commit_sha1)
139-
140- def __ne__(self, other):
141- return not self == other
142-
143- def __hash__(self):
144- return hash(self.repository) ^ hash(self.path) ^ hash(self.commit_sha1)
145-
146
147 @implementer(IGitRef)
148 @provider(IGitRefRemoteSet)
149
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 from lp.code.interfaces.revision import IRevisionSet
155 from lp.code.mail.branch import send_git_repository_modified_notifications
156 from lp.code.model.branchmergeproposal import BranchMergeProposal
157-from lp.code.model.gitref import GitRef
158+from lp.code.model.gitref import (
159+ GitRef,
160+ GitRefDefault,
161+ )
162 from lp.code.model.gitsubscription import GitSubscription
163 from lp.registry.enums import PersonVisibility
164 from lp.registry.errors import CannotChangeInformationType
165@@ -515,6 +518,8 @@
166 self.getInternalPath(), default_branch=ref.path)
167
168 def getRefByPath(self, path):
169+ if path == u"HEAD":
170+ return GitRefDefault(self)
171 paths = [path]
172 if not path.startswith(u"refs/heads/"):
173 paths.append(u"refs/heads/%s" % path)
174
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 self.assertEqual(ref, ref.repository.getRefByPath(u"master"))
180 self.assertIsNone(ref.repository.getRefByPath(u"other"))
181
182+ def test_getRefByPath_HEAD(self):
183+ # The special ref path "HEAD" always refers to the current default
184+ # branch.
185+ [ref] = self.factory.makeGitRefs(paths=[u"refs/heads/master"])
186+ ref_HEAD = ref.repository.getRefByPath(u"HEAD")
187+ self.assertEqual(ref.repository, ref_HEAD.repository)
188+ self.assertEqual(u"HEAD", ref_HEAD.path)
189+ self.assertRaises(NotFoundError, getattr, ref_HEAD, "commit_sha1")
190+ removeSecurityProxy(ref.repository)._default_branch = (
191+ u"refs/heads/missing")
192+ self.assertRaises(NotFoundError, getattr, ref_HEAD, "commit_sha1")
193+ removeSecurityProxy(ref.repository)._default_branch = ref.path
194+ self.assertEqual(ref.commit_sha1, ref_HEAD.commit_sha1)
195+
196 def test_planRefChanges(self):
197 # planRefChanges copes with planning changes to refs in a repository
198 # where some refs have been created, some deleted, and some changed.
199@@ -2700,6 +2714,7 @@
200 repository_db = self.factory.makeGitRepository()
201 ref_dbs = self.factory.makeGitRefs(
202 repository=repository_db, paths=[u"refs/heads/a", u"refs/heads/b"])
203+ removeSecurityProxy(repository_db)._default_branch = u"refs/heads/a"
204 repository_url = api_url(repository_db)
205 ref_urls = [api_url(ref_db) for ref_db in ref_dbs]
206 webservice = webservice_for_person(
207@@ -2710,6 +2725,7 @@
208 ("refs/heads/a", ref_urls[0]),
209 ("b", ref_urls[1]),
210 ("refs/heads/b", ref_urls[1]),
211+ ("HEAD", "%s/+ref/HEAD" % repository_url),
212 ):
213 response = webservice.named_get(
214 repository_url, "getRefByPath", path=path)
215
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 @@
220-# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
221+# Copyright 2015-2017 Canonical Ltd. This software is licensed under the
222 # GNU Affero General Public License version 3 (see the file LICENSE).
223
224 """An `IBuildFarmJobBehaviour` for `SnapBuild`.
225@@ -114,7 +114,8 @@
226 # "git clone -b" doesn't accept full ref names. If this becomes
227 # a problem then we could change launchpad-buildd to do "git
228 # clone" followed by "git checkout" instead.
229- args["git_path"] = build.snap.git_ref.name
230+ if build.snap.git_path != u"HEAD":
231+ args["git_path"] = build.snap.git_ref.name
232 else:
233 raise CannotBuild(
234 "Source branch/repository for ~%s/%s has been deleted." %
235
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 from twisted.internet import defer
241 from twisted.trial.unittest import TestCase as TrialTestCase
242 from zope.component import getUtility
243+from zope.security.proxy import removeSecurityProxy
244
245 from lp.buildmaster.enums import BuildStatus
246 from lp.buildmaster.interfaces.builder import CannotBuild
247@@ -245,6 +246,26 @@
248 }, args)
249
250 @defer.inlineCallbacks
251+ def test_extraBuildArgs_git_HEAD(self):
252+ # _extraBuildArgs returns appropriate arguments if asked to build a
253+ # job for the default branch in a Launchpad-hosted Git repository.
254+ [ref] = self.factory.makeGitRefs()
255+ removeSecurityProxy(ref.repository)._default_branch = ref.path
256+ job = self.makeJob(git_ref=ref.repository.getRefByPath(u"HEAD"))
257+ expected_archives = get_sources_list_for_building(
258+ job.build, job.build.distro_arch_series, None)
259+ args = yield job._extraBuildArgs()
260+ self.assertEqual({
261+ "archive_private": False,
262+ "archives": expected_archives,
263+ "arch_tag": "i386",
264+ "git_repository": ref.repository.git_https_url,
265+ "name": u"test-snap",
266+ "proxy_url": self.proxy_url,
267+ "revocation_endpoint": self.revocation_endpoint,
268+ }, args)
269+
270+ @defer.inlineCallbacks
271 def test_extraBuildArgs_git_url(self):
272 # _extraBuildArgs returns appropriate arguments if asked to build a
273 # job for a Git branch backed by a URL for an external repository.
274@@ -267,6 +288,26 @@
275 }, args)
276
277 @defer.inlineCallbacks
278+ def test_extraBuildArgs_git_url_HEAD(self):
279+ # _extraBuildArgs returns appropriate arguments if asked to build a
280+ # job for the default branch in an external Git repository.
281+ url = u"https://git.example.org/foo"
282+ ref = self.factory.makeGitRefRemote(repository_url=url, path=u"HEAD")
283+ job = self.makeJob(git_ref=ref)
284+ expected_archives = get_sources_list_for_building(
285+ job.build, job.build.distro_arch_series, None)
286+ args = yield job._extraBuildArgs()
287+ self.assertEqual({
288+ "archive_private": False,
289+ "archives": expected_archives,
290+ "arch_tag": "i386",
291+ "git_repository": url,
292+ "name": u"test-snap",
293+ "proxy_url": self.proxy_url,
294+ "revocation_endpoint": self.revocation_endpoint,
295+ }, args)
296+
297+ @defer.inlineCallbacks
298 def test_extraBuildArgs_proxy_url_set(self):
299 job = self.makeJob()
300 build_request = yield job.composeBuildRequest(None)