← Back to team overview

launchpad-reviewers team mailing list archive

Re: [Merge] lp:~cjwatson/launchpad/git-default-branch into lp:launchpad

 

Review: Approve code



Diff comments:

> === modified file 'lib/lp/code/browser/gitrepository.py'
> --- lib/lp/code/browser/gitrepository.py	2015-06-12 08:04:42 +0000
> +++ lib/lp/code/browser/gitrepository.py	2015-06-13 01:58:23 +0000
> @@ -21,7 +21,10 @@
>  
>  from lazr.lifecycle.event import ObjectModifiedEvent
>  from lazr.lifecycle.snapshot import Snapshot
> -from lazr.restful.interface import copy_field
> +from lazr.restful.interface import (
> +    copy_field,
> +    use_template,
> +    )
>  from storm.expr import Desc
>  from zope.event import notify
>  from zope.interface import (
> @@ -266,6 +269,7 @@
>              This is necessary to make various fields editable that are not
>              normally editable through the interface.
>              """
> +            use_template(IGitRepository, include=["default_branch"])
>              information_type = copy_field(
>                  IGitRepository["information_type"], readonly=False,
>                  vocabulary=InformationTypeVocabulary(types=info_types))
> @@ -378,6 +382,7 @@
>          "owner",
>          "name",
>          "information_type",
> +        "default_branch",
>          ]
>  
>      custom_widget("information_type", LaunchpadRadioWidgetWithDescription)
> @@ -416,6 +421,14 @@
>                          (owner.displayname, target.displayname))
>                  except GitRepositoryExists as e:
>                      self._setRepositoryExists(e.existing_repository)
> +        if "default_branch" in data:
> +            default_branch = data["default_branch"]
> +            if (default_branch is not None and
> +                    self.context.getRefByPath(default_branch) is None):
> +                self.setFieldError(
> +                    "default_branch",
> +                    "This repository does not contain a reference named "
> +                    "'%s'." % default_branch)
>  
>  
>  class GitRepositoryDeletionView(LaunchpadFormView):
> 
> === modified file 'lib/lp/code/browser/tests/test_gitrepository.py'
> --- lib/lp/code/browser/tests/test_gitrepository.py	2015-06-05 13:10:18 +0000
> +++ lib/lp/code/browser/tests/test_gitrepository.py	2015-06-13 01:58:23 +0000
> @@ -15,6 +15,7 @@
>      DocTestMatches,
>      Equals,
>      )
> +import transaction
>  from zope.component import getUtility
>  from zope.publisher.interfaces import NotFound
>  from zope.security.proxy import removeSecurityProxy
> @@ -22,6 +23,7 @@
>  from lp.app.enums import InformationType
>  from lp.app.interfaces.launchpad import ILaunchpadCelebrities
>  from lp.app.interfaces.services import IService
> +from lp.code.interfaces.githosting import IGitHostingClient
>  from lp.code.interfaces.revision import IRevisionSet
>  from lp.registry.enums import BranchSharingPolicy
>  from lp.registry.interfaces.person import PersonVisibility
> @@ -37,6 +39,8 @@
>      record_two_runs,
>      TestCaseWithFactory,
>      )
> +from lp.testing.fakemethod import FakeMethod
> +from lp.testing.fixture import ZopeUtilityFixture
>  from lp.testing.layers import DatabaseFunctionalLayer
>  from lp.testing.matchers import HasQueryCount
>  from lp.testing.pages import (
> @@ -343,6 +347,50 @@
>              self.assertEqual(
>                  repository.information_type, InformationType.PUBLICSECURITY)
>  
> +    def test_change_default_branch(self):
> +        # An authorised user can change the default branch to one that
> +        # exists.  They may omit "refs/heads/".
> +        hosting_client = FakeMethod()
> +        hosting_client.setProperties = FakeMethod()
> +        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
> +        person = self.factory.makePerson()
> +        repository = self.factory.makeGitRepository(owner=person)
> +        master, new = self.factory.makeGitRefs(
> +            repository=repository,
> +            paths=[u"refs/heads/master", u"refs/heads/new"])
> +        removeSecurityProxy(repository)._default_branch = u"refs/heads/master"
> +        browser = self.getUserBrowser(
> +            canonical_url(repository) + "/+edit", user=person)
> +        browser.getControl(name="field.default_branch").value = u"new"
> +        browser.getControl("Change Git Repository").click()
> +        with person_logged_in(person):
> +            self.assertEqual(
> +                [((repository.getInternalPath(),),
> +                 {u"default_branch": u"refs/heads/new"})],
> +                hosting_client.setProperties.calls)
> +            self.assertEqual(u"refs/heads/new", repository.default_branch)
> +
> +    def test_change_default_branch_nonexistent(self):
> +        # Trying to change the default branch to one that doesn't exist
> +        # displays an error.
> +        person = self.factory.makePerson()
> +        repository = self.factory.makeGitRepository(owner=person)
> +        [master] = self.factory.makeGitRefs(
> +            repository=repository, paths=[u"refs/heads/master"])
> +        removeSecurityProxy(repository)._default_branch = u"refs/heads/master"
> +        form = {
> +            "field.default_branch": "refs/heads/new",
> +            "field.actions.change": "Change Git Repository",
> +            }
> +        transaction.commit()
> +        with person_logged_in(person):
> +            view = create_initialized_view(repository, name="+edit", form=form)
> +        self.assertEqual(
> +            ["This repository does not contain a reference named "
> +             "'refs/heads/new'."],
> +            view.errors)
> +        self.assertEqual(u"refs/heads/master", repository.default_branch)
> +
>  
>  class TestGitRepositoryEditViewInformationTypes(TestCaseWithFactory):
>      """Tests for GitRepositoryEditView.getInformationTypesToShow."""
> 
> === modified file 'lib/lp/code/configure.zcml'
> --- lib/lp/code/configure.zcml	2015-06-13 01:58:23 +0000
> +++ lib/lp/code/configure.zcml	2015-06-13 01:58:23 +0000
> @@ -787,7 +787,8 @@
>          permission="launchpad.View"
>          interface="lp.app.interfaces.launchpad.IPrivacy
>                     lp.code.interfaces.gitrepository.IGitRepositoryView
> -                   lp.code.interfaces.gitrepository.IGitRepositoryModerateAttributes" />
> +                   lp.code.interfaces.gitrepository.IGitRepositoryModerateAttributes
> +                   lp.code.interfaces.gitrepository.IGitRepositoryEditableAttributes" />
>      <require
>          permission="launchpad.Moderate"
>          interface="lp.code.interfaces.gitrepository.IGitRepositoryModerate"
> @@ -795,7 +796,8 @@
>          set_attributes="date_last_modified" />
>      <require
>          permission="launchpad.Edit"
> -        interface="lp.code.interfaces.gitrepository.IGitRepositoryEdit" />
> +        interface="lp.code.interfaces.gitrepository.IGitRepositoryEdit"
> +        set_schema="lp.code.interfaces.gitrepository.IGitRepositoryEditableAttributes" />
>    </class>
>    <subscriber
>        for="lp.code.interfaces.gitrepository.IGitRepository zope.lifecycleevent.interfaces.IObjectModifiedEvent"
> 
> === modified file 'lib/lp/code/errors.py'
> --- lib/lp/code/errors.py	2015-05-19 11:29:24 +0000
> +++ lib/lp/code/errors.py	2015-06-13 01:58:23 +0000
> @@ -43,6 +43,7 @@
>      'InvalidNamespace',
>      'NoLinkedBranch',
>      'NoSuchBranch',
> +    'NoSuchGitReference',
>      'NoSuchGitRepository',
>      'PrivateBranchRecipe',
>      'ReviewNotPending',
> @@ -61,7 +62,10 @@
>  from bzrlib.plugins.builder.recipe import RecipeParseError
>  from lazr.restful.declarations import error_status
>  
> -from lp.app.errors import NameLookupFailed
> +from lp.app.errors import (
> +    NameLookupFailed,
> +    NotFoundError,
> +    )
>  
>  # Annotate the RecipeParseError's with a 400 webservice status.
>  error_status(httplib.BAD_REQUEST)(RecipeParseError)
> @@ -402,6 +406,21 @@
>      _message_prefix = "No such Git repository"
>  
>  
> +class NoSuchGitReference(NotFoundError):
> +    """Raised when we try to look up a Git reference that does not exist."""
> +
> +    def __init__(self, repository, path):
> +        self.repository = repository
> +        self.path = path
> +        self.message = (
> +            "The repository at %s does not contain a reference named '%s'." %
> +            (repository.display_name, path))
> +        NotFoundError.__init__(self, self.message)
> +
> +    def __str__(self):
> +        return self.message
> +
> +
>  @error_status(httplib.CONFLICT)
>  class GitDefaultConflict(Exception):
>      """Raised when trying to set a Git repository as the default for
> 
> === modified file 'lib/lp/code/interfaces/githosting.py'
> --- lib/lp/code/interfaces/githosting.py	2015-06-13 01:58:23 +0000
> +++ lib/lp/code/interfaces/githosting.py	2015-06-13 01:58:23 +0000
> @@ -23,6 +23,20 @@
>              other physical path.
>          """
>  
> +    def getProperties(path):
> +        """Get properties of this repository.
> +
> +        :param path: Physical path of the repository on the hosting service.
> +        :return: A dict of properties.
> +        """
> +
> +    def setProperties(path, **props):
> +        """Set properties of this repository.
> +
> +        :param path: Physical path of the repository on the hosting service.
> +        :param props: Properties to set.
> +        """
> +
>      def getRefs(path):
>          """Get all refs in this repository.
>  
> 
> === modified file 'lib/lp/code/interfaces/gitrepository.py'
> --- lib/lp/code/interfaces/gitrepository.py	2015-06-13 01:58:23 +0000
> +++ lib/lp/code/interfaces/gitrepository.py	2015-06-13 01:58:23 +0000
> @@ -552,6 +552,19 @@
>          """
>  
>  
> +class IGitRepositoryEditableAttributes(Interface):
> +    """IGitRepository attributes that can be edited.
> +
> +    These attributes need launchpad.View to see, and launchpad.Edit to change.
> +    """
> +
> +    default_branch = exported(TextLine(
> +        title=_("Default branch"), required=False, readonly=False,
> +        description=_(
> +            "The full path to the default branch for this repository, e.g. "
> +            "refs/heads/master.")))
> +
> +
>  class IGitRepositoryEdit(Interface):
>      """IGitRepository methods that require launchpad.Edit permission."""
>  
> @@ -619,7 +632,8 @@
>  
>  
>  class IGitRepository(IGitRepositoryView, IGitRepositoryModerateAttributes,
> -                     IGitRepositoryModerate, IGitRepositoryEdit):
> +                     IGitRepositoryModerate, IGitRepositoryEditableAttributes,
> +                     IGitRepositoryEdit):
>      """A Git repository."""
>  
>      # Mark repositories as exported entries for the Launchpad API.
> 
> === modified file 'lib/lp/code/model/githosting.py'
> --- lib/lp/code/model/githosting.py	2015-06-13 01:58:23 +0000
> +++ lib/lp/code/model/githosting.py	2015-06-13 01:58:23 +0000
> @@ -64,6 +64,9 @@
>      def _post(self, path, **kwargs):
>          return self._request("post", path, **kwargs)
>  
> +    def _patch(self, path, **kwargs):
> +        return self._request("patch", path, **kwargs)
> +
>      def _delete(self, path, **kwargs):
>          return self._request("delete", path, **kwargs)
>  
> @@ -78,6 +81,22 @@
>              raise GitRepositoryCreationFault(
>                  "Failed to create Git repository: %s" % unicode(e))
>  
> +    def getProperties(self, path):
> +        """See `IGitHostingClient`."""
> +        try:
> +            return self._get("/repo/%s" % path)
> +        except Exception as e:
> +            raise GitRepositoryScanFault(
> +                "Failed to get properties of Git repository: %s" % unicode(e))
> +
> +    def setProperties(self, path, **props):
> +        """See `IGitHostingClient`."""
> +        try:
> +            self._patch("/repo/%s" % path, json_data=props)
> +        except Exception as e:
> +            raise GitRepositoryScanFault(
> +                "Failed to set properties of Git repository: %s" % unicode(e))
> +
>      def getRefs(self, path):
>          try:
>              return self._get("/repo/%s/refs" % path)
> 
> === modified file 'lib/lp/code/model/gitjob.py'
> --- lib/lp/code/model/gitjob.py	2015-06-13 01:58:23 +0000
> +++ lib/lp/code/model/gitjob.py	2015-06-13 01:58:23 +0000
> @@ -28,6 +28,7 @@
>      classProvides,
>      implements,
>      )
> +from zope.security.proxy import removeSecurityProxy
>  
>  from lp.app.errors import NotFoundError
>  from lp.code.interfaces.githosting import IGitHostingClient
> @@ -212,6 +213,12 @@
>                      hosting_path, refs_to_upsert, logger=log)
>                  self.repository.synchroniseRefs(
>                      refs_to_upsert, refs_to_remove, logger=log)
> +                props = getUtility(IGitHostingClient).getProperties(
> +                    hosting_path)
> +                # We don't want ref canonicalisation, nor do we want to send
> +                # this change back to the hosting service.
> +                removeSecurityProxy(self.repository)._default_branch = (
> +                    props["default_branch"])
>          except LostObjectError:
>              log.info(
>                  "Skipping repository %s because it has been deleted." %
> 
> === modified file 'lib/lp/code/model/gitrepository.py'
> --- lib/lp/code/model/gitrepository.py	2015-06-13 01:58:23 +0000
> +++ lib/lp/code/model/gitrepository.py	2015-06-13 01:58:23 +0000
> @@ -72,6 +72,7 @@
>      CannotDeleteGitRepository,
>      GitDefaultConflict,
>      GitTargetError,
> +    NoSuchGitReference,
>      )
>  from lp.code.event.git import GitRefsUpdatedEvent
>  from lp.code.interfaces.branchmergeproposal import (
> @@ -206,6 +207,8 @@
>      owner_default = Bool(name='owner_default', allow_none=False)
>      target_default = Bool(name='target_default', allow_none=False)
>  
> +    _default_branch = Unicode(name='default_branch', allow_none=True)
> +
>      def __init__(self, registrant, owner, target, name, information_type,
>                   date_created, reviewer=None, description=None):
>          super(GitRepository, self).__init__()
> @@ -407,6 +410,21 @@
>              GitRef.repository_id == self.id,
>              GitRef.path.startswith(u"refs/heads/")).order_by(GitRef.path)
>  
> +    @property
> +    def default_branch(self):
> +        """See `IGitRepository`."""
> +        return self._default_branch
> +
> +    @default_branch.setter
> +    def default_branch(self, value):
> +        """See `IGitRepository`."""
> +        ref = self.getRefByPath(value)
> +        if ref is None:
> +            raise NoSuchGitReference(self, value)
> +        self._default_branch = ref.path
> +        getUtility(IGitHostingClient).setProperties(
> +            self.getInternalPath(), default_branch=ref.path)

Do we want to short-circuit if the value hasn't changed?

> +
>      def getRefByPath(self, path):
>          paths = [path]
>          if not path.startswith(u"refs/heads/"):
> 
> === modified file 'lib/lp/code/model/tests/test_gitjob.py'
> --- lib/lp/code/model/tests/test_gitjob.py	2015-06-13 01:58:23 +0000
> +++ lib/lp/code/model/tests/test_gitjob.py	2015-06-13 01:58:23 +0000
> @@ -148,9 +148,12 @@
>          author_date_gen = time_counter(author_date_start, timedelta(days=1))
>          hosting_client.getCommits = FakeMethod(
>              result=self.makeFakeCommits(author, author_date_gen, paths))
> +        hosting_client.getProperties = FakeMethod(
> +            result={u"default_branch": u"refs/heads/master"})
>          with dbuser("branchscanner"):
>              JobRunner([job]).runAll()
>          self.assertRefsMatch(repository.refs, repository, paths)
> +        self.assertEqual(u"refs/heads/master", repository.default_branch)
>  
>      def test_logs_bad_ref_info(self):
>          hosting_client = FakeGitHostingClient()
> 
> === modified file 'lib/lp/code/model/tests/test_gitrepository.py'
> --- lib/lp/code/model/tests/test_gitrepository.py	2015-06-13 01:58:23 +0000
> +++ lib/lp/code/model/tests/test_gitrepository.py	2015-06-13 01:58:23 +0000
> @@ -1217,6 +1217,23 @@
>                  ) for path, sha1 in expected_sha1s]
>          self.assertThat(repository.refs, MatchesSetwise(*matchers))
>  
> +    def test_set_default_branch(self):
> +        hosting_client = FakeMethod()
> +        hosting_client.setProperties = FakeMethod()
> +        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
> +        repository = self.factory.makeGitRepository()
> +        self.factory.makeGitRefs(
> +            repository=repository,
> +            paths=(u"refs/heads/master", u"refs/heads/new"))
> +        removeSecurityProxy(repository)._default_branch = u"refs/heads/master"
> +        with person_logged_in(repository.owner):
> +            repository.default_branch = u"new"
> +        self.assertEqual(
> +            [((repository.getInternalPath(),),
> +             {u"default_branch": u"refs/heads/new"})],
> +            hosting_client.setProperties.calls)
> +        self.assertEqual(u"refs/heads/new", repository.default_branch)
> +
>  
>  class TestGitRepositoryGetAllowedInformationTypes(TestCaseWithFactory):
>      """Test `IGitRepository.getAllowedInformationTypes`."""
> 


-- 
https://code.launchpad.net/~cjwatson/launchpad/git-default-branch/+merge/261892
Your team Launchpad code reviewers is subscribed to branch lp:launchpad.


References