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
=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml 2016-12-02 12:04:11 +0000
+++ lib/lp/code/configure.zcml 2017-05-05 12:17:30 +0000
@@ -1,4 +1,4 @@
1<!-- Copyright 2009-2016 Canonical Ltd. This software is licensed under the1<!-- Copyright 2009-2017 Canonical Ltd. This software is licensed under the
2 GNU Affero General Public License version 3 (see the file LICENSE).2 GNU Affero General Public License version 3 (see the file LICENSE).
3-->3-->
44
@@ -896,6 +896,11 @@
896 permission="launchpad.View"896 permission="launchpad.View"
897 interface="lp.code.interfaces.gitref.IGitRef" />897 interface="lp.code.interfaces.gitref.IGitRef" />
898 </class>898 </class>
899 <class class="lp.code.model.gitref.GitRefDefault">
900 <require
901 permission="launchpad.View"
902 interface="lp.code.interfaces.gitref.IGitRef" />
903 </class>
899 <class class="lp.code.model.gitref.GitRefFrozen">904 <class class="lp.code.model.gitref.GitRefFrozen">
900 <require905 <require
901 permission="launchpad.View"906 permission="launchpad.View"
902907
=== modified file 'lib/lp/code/model/gitref.py'
--- lib/lp/code/model/gitref.py 2016-12-05 14:46:40 +0000
+++ lib/lp/code/model/gitref.py 2017-05-05 12:17:30 +0000
@@ -1,4 +1,4 @@
1# Copyright 2015-2016 Canonical Ltd. This software is licensed under the1# Copyright 2015-2017 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
@@ -536,8 +536,72 @@
536 commit_message=commit_message, review_requests=review_requests)536 commit_message=commit_message, review_requests=review_requests)
537537
538538
539@implementer(IGitRef)539class GitRefDatabaseBackedMixin(GitRefMixin):
540class GitRefFrozen(GitRefMixin):540 """A mixin for virtual Git references backed by a database record."""
541
542 @property
543 def _non_database_attributes(self):
544 """A sequence of attributes not backed by the database."""
545 raise NotImplementedError()
546
547 @property
548 def _self_in_database(self):
549 """Return the equivalent database-backed record of self."""
550 raise NotImplementedError()
551
552 def __getattr__(self, name):
553 return getattr(self._self_in_database, name)
554
555 def __setattr__(self, name, value):
556 if name in self._non_database_attributes:
557 self.__dict__[name] = value
558 else:
559 setattr(self._self_in_database, name, value)
560
561 def __eq__(self, other):
562 return (
563 self.repository == other.repository and
564 self.path == other.path and
565 self.commit_sha1 == other.commit_sha1)
566
567 def __ne__(self, other):
568 return not self == other
569
570 def __hash__(self):
571 return hash(self.repository) ^ hash(self.path) ^ hash(self.commit_sha1)
572
573
574@implementer(IGitRef)
575class GitRefDefault(GitRefDatabaseBackedMixin):
576 """A reference to the default branch in a Git repository.
577
578 This always refers to whatever the default branch currently is, even if
579 it changes later.
580 """
581
582 def __init__(self, repository):
583 self.repository_id = repository.id
584 self.repository = repository
585 self.path = u"HEAD"
586
587 _non_database_attributes = ("repository_id", "repository", "path")
588
589 @property
590 def _self_in_database(self):
591 """See `GitRefDatabaseBackedMixin`."""
592 path = self.repository.default_branch
593 if path is None:
594 raise NotFoundError("Repository '%s' has no default branch")
595 ref = IStore(GitRef).get(GitRef, (self.repository_id, path))
596 if ref is None:
597 raise NotFoundError(
598 "Repository '%s' has default branch '%s', but there is no "
599 "such reference" % (self.repository, path))
600 return ref
601
602
603@implementer(IGitRef)
604class GitRefFrozen(GitRefDatabaseBackedMixin):
541 """A frozen Git reference.605 """A frozen Git reference.
542606
543 This is like a GitRef, but is frozen at a particular commit, even if the607 This is like a GitRef, but is frozen at a particular commit, even if the
@@ -554,9 +618,12 @@
554 self.path = path618 self.path = path
555 self.commit_sha1 = commit_sha1619 self.commit_sha1 = commit_sha1
556620
621 _non_database_attributes = (
622 "repository_id", "repository", "path", "commit_sha1")
623
557 @property624 @property
558 def _self_in_database(self):625 def _self_in_database(self):
559 """Return the equivalent database-backed record of self."""626 """See `GitRefDatabaseBackedMixin`."""
560 ref = IStore(GitRef).get(GitRef, (self.repository_id, self.path))627 ref = IStore(GitRef).get(GitRef, (self.repository_id, self.path))
561 if ref is None:628 if ref is None:
562 raise NotFoundError(629 raise NotFoundError(
@@ -564,27 +631,6 @@
564 "'%s'" % (self.repository, self.path))631 "'%s'" % (self.repository, self.path))
565 return ref632 return ref
566633
567 def __getattr__(self, name):
568 return getattr(self._self_in_database, name)
569
570 def __setattr__(self, name, value):
571 if name in ("repository_id", "repository", "path", "commit_sha1"):
572 self.__dict__[name] = value
573 else:
574 setattr(self._self_in_database, name, value)
575
576 def __eq__(self, other):
577 return (
578 self.repository == other.repository and
579 self.path == other.path and
580 self.commit_sha1 == other.commit_sha1)
581
582 def __ne__(self, other):
583 return not self == other
584
585 def __hash__(self):
586 return hash(self.repository) ^ hash(self.path) ^ hash(self.commit_sha1)
587
588634
589@implementer(IGitRef)635@implementer(IGitRef)
590@provider(IGitRefRemoteSet)636@provider(IGitRefRemoteSet)
591637
=== modified file 'lib/lp/code/model/gitrepository.py'
--- lib/lp/code/model/gitrepository.py 2017-02-10 12:52:07 +0000
+++ lib/lp/code/model/gitrepository.py 2017-05-05 12:17:30 +0000
@@ -107,7 +107,10 @@
107from lp.code.interfaces.revision import IRevisionSet107from lp.code.interfaces.revision import IRevisionSet
108from lp.code.mail.branch import send_git_repository_modified_notifications108from lp.code.mail.branch import send_git_repository_modified_notifications
109from lp.code.model.branchmergeproposal import BranchMergeProposal109from lp.code.model.branchmergeproposal import BranchMergeProposal
110from lp.code.model.gitref import GitRef110from lp.code.model.gitref import (
111 GitRef,
112 GitRefDefault,
113 )
111from lp.code.model.gitsubscription import GitSubscription114from lp.code.model.gitsubscription import GitSubscription
112from lp.registry.enums import PersonVisibility115from lp.registry.enums import PersonVisibility
113from lp.registry.errors import CannotChangeInformationType116from lp.registry.errors import CannotChangeInformationType
@@ -515,6 +518,8 @@
515 self.getInternalPath(), default_branch=ref.path)518 self.getInternalPath(), default_branch=ref.path)
516519
517 def getRefByPath(self, path):520 def getRefByPath(self, path):
521 if path == u"HEAD":
522 return GitRefDefault(self)
518 paths = [path]523 paths = [path]
519 if not path.startswith(u"refs/heads/"):524 if not path.startswith(u"refs/heads/"):
520 paths.append(u"refs/heads/%s" % path)525 paths.append(u"refs/heads/%s" % path)
521526
=== modified file 'lib/lp/code/model/tests/test_gitrepository.py'
--- lib/lp/code/model/tests/test_gitrepository.py 2017-02-10 12:52:07 +0000
+++ lib/lp/code/model/tests/test_gitrepository.py 2017-05-05 12:17:30 +0000
@@ -1182,6 +1182,20 @@
1182 self.assertEqual(ref, ref.repository.getRefByPath(u"master"))1182 self.assertEqual(ref, ref.repository.getRefByPath(u"master"))
1183 self.assertIsNone(ref.repository.getRefByPath(u"other"))1183 self.assertIsNone(ref.repository.getRefByPath(u"other"))
11841184
1185 def test_getRefByPath_HEAD(self):
1186 # The special ref path "HEAD" always refers to the current default
1187 # branch.
1188 [ref] = self.factory.makeGitRefs(paths=[u"refs/heads/master"])
1189 ref_HEAD = ref.repository.getRefByPath(u"HEAD")
1190 self.assertEqual(ref.repository, ref_HEAD.repository)
1191 self.assertEqual(u"HEAD", ref_HEAD.path)
1192 self.assertRaises(NotFoundError, getattr, ref_HEAD, "commit_sha1")
1193 removeSecurityProxy(ref.repository)._default_branch = (
1194 u"refs/heads/missing")
1195 self.assertRaises(NotFoundError, getattr, ref_HEAD, "commit_sha1")
1196 removeSecurityProxy(ref.repository)._default_branch = ref.path
1197 self.assertEqual(ref.commit_sha1, ref_HEAD.commit_sha1)
1198
1185 def test_planRefChanges(self):1199 def test_planRefChanges(self):
1186 # planRefChanges copes with planning changes to refs in a repository1200 # planRefChanges copes with planning changes to refs in a repository
1187 # where some refs have been created, some deleted, and some changed.1201 # where some refs have been created, some deleted, and some changed.
@@ -2700,6 +2714,7 @@
2700 repository_db = self.factory.makeGitRepository()2714 repository_db = self.factory.makeGitRepository()
2701 ref_dbs = self.factory.makeGitRefs(2715 ref_dbs = self.factory.makeGitRefs(
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"])
2717 removeSecurityProxy(repository_db)._default_branch = u"refs/heads/a"
2703 repository_url = api_url(repository_db)2718 repository_url = api_url(repository_db)
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]
2705 webservice = webservice_for_person(2720 webservice = webservice_for_person(
@@ -2710,6 +2725,7 @@
2710 ("refs/heads/a", ref_urls[0]),2725 ("refs/heads/a", ref_urls[0]),
2711 ("b", ref_urls[1]),2726 ("b", ref_urls[1]),
2712 ("refs/heads/b", ref_urls[1]),2727 ("refs/heads/b", ref_urls[1]),
2728 ("HEAD", "%s/+ref/HEAD" % repository_url),
2713 ):2729 ):
2714 response = webservice.named_get(2730 response = webservice.named_get(
2715 repository_url, "getRefByPath", path=path)2731 repository_url, "getRefByPath", path=path)
27162732
=== modified file 'lib/lp/snappy/model/snapbuildbehaviour.py'
--- lib/lp/snappy/model/snapbuildbehaviour.py 2016-12-05 23:11:57 +0000
+++ lib/lp/snappy/model/snapbuildbehaviour.py 2017-05-05 12:17:30 +0000
@@ -1,4 +1,4 @@
1# Copyright 2015-2016 Canonical Ltd. This software is licensed under the1# Copyright 2015-2017 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""An `IBuildFarmJobBehaviour` for `SnapBuild`.4"""An `IBuildFarmJobBehaviour` for `SnapBuild`.
@@ -114,7 +114,8 @@
114 # "git clone -b" doesn't accept full ref names. If this becomes114 # "git clone -b" doesn't accept full ref names. If this becomes
115 # a problem then we could change launchpad-buildd to do "git115 # a problem then we could change launchpad-buildd to do "git
116 # clone" followed by "git checkout" instead.116 # clone" followed by "git checkout" instead.
117 args["git_path"] = build.snap.git_ref.name117 if build.snap.git_path != u"HEAD":
118 args["git_path"] = build.snap.git_ref.name
118 else:119 else:
119 raise CannotBuild(120 raise CannotBuild(
120 "Source branch/repository for ~%s/%s has been deleted." %121 "Source branch/repository for ~%s/%s has been deleted." %
121122
=== modified file 'lib/lp/snappy/tests/test_snapbuildbehaviour.py'
--- lib/lp/snappy/tests/test_snapbuildbehaviour.py 2017-01-27 12:44:41 +0000
+++ lib/lp/snappy/tests/test_snapbuildbehaviour.py 2017-05-05 12:17:30 +0000
@@ -22,6 +22,7 @@
22from twisted.internet import defer22from twisted.internet import defer
23from twisted.trial.unittest import TestCase as TrialTestCase23from twisted.trial.unittest import TestCase as TrialTestCase
24from zope.component import getUtility24from zope.component import getUtility
25from zope.security.proxy import removeSecurityProxy
2526
26from lp.buildmaster.enums import BuildStatus27from lp.buildmaster.enums import BuildStatus
27from lp.buildmaster.interfaces.builder import CannotBuild28from lp.buildmaster.interfaces.builder import CannotBuild
@@ -245,6 +246,26 @@
245 }, args)246 }, args)
246247
247 @defer.inlineCallbacks248 @defer.inlineCallbacks
249 def test_extraBuildArgs_git_HEAD(self):
250 # _extraBuildArgs returns appropriate arguments if asked to build a
251 # job for the default branch in a Launchpad-hosted Git repository.
252 [ref] = self.factory.makeGitRefs()
253 removeSecurityProxy(ref.repository)._default_branch = ref.path
254 job = self.makeJob(git_ref=ref.repository.getRefByPath(u"HEAD"))
255 expected_archives = get_sources_list_for_building(
256 job.build, job.build.distro_arch_series, None)
257 args = yield job._extraBuildArgs()
258 self.assertEqual({
259 "archive_private": False,
260 "archives": expected_archives,
261 "arch_tag": "i386",
262 "git_repository": ref.repository.git_https_url,
263 "name": u"test-snap",
264 "proxy_url": self.proxy_url,
265 "revocation_endpoint": self.revocation_endpoint,
266 }, args)
267
268 @defer.inlineCallbacks
248 def test_extraBuildArgs_git_url(self):269 def test_extraBuildArgs_git_url(self):
249 # _extraBuildArgs returns appropriate arguments if asked to build a270 # _extraBuildArgs returns appropriate arguments if asked to build a
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.
@@ -267,6 +288,26 @@
267 }, args)288 }, args)
268289
269 @defer.inlineCallbacks290 @defer.inlineCallbacks
291 def test_extraBuildArgs_git_url_HEAD(self):
292 # _extraBuildArgs returns appropriate arguments if asked to build a
293 # job for the default branch in an external Git repository.
294 url = u"https://git.example.org/foo"
295 ref = self.factory.makeGitRefRemote(repository_url=url, path=u"HEAD")
296 job = self.makeJob(git_ref=ref)
297 expected_archives = get_sources_list_for_building(
298 job.build, job.build.distro_arch_series, None)
299 args = yield job._extraBuildArgs()
300 self.assertEqual({
301 "archive_private": False,
302 "archives": expected_archives,
303 "arch_tag": "i386",
304 "git_repository": url,
305 "name": u"test-snap",
306 "proxy_url": self.proxy_url,
307 "revocation_endpoint": self.revocation_endpoint,
308 }, args)
309
310 @defer.inlineCallbacks
270 def test_extraBuildArgs_proxy_url_set(self):311 def test_extraBuildArgs_proxy_url_set(self):
271 job = self.makeJob()312 job = self.makeJob()
272 build_request = yield job.composeBuildRequest(None)313 build_request = yield job.composeBuildRequest(None)