← Back to team overview

launchpad-reviewers team mailing list archive

Re: [Merge] lp:~cjwatson/launchpad/git-mp-basic-browser into lp:launchpad

 

Review: Approve code

One bit of useless code, otherwise looks good.

Diff comments:

> === modified file 'lib/lp/code/browser/branchmergeproposal.py'
> --- lib/lp/code/browser/branchmergeproposal.py	2015-04-22 16:41:41 +0000
> +++ lib/lp/code/browser/branchmergeproposal.py	2015-04-28 16:48:46 +0000
> @@ -86,6 +86,7 @@
>      InvalidBranchMergeProposal,
>      WrongBranchMergeProposal,
>      )
> +from lp.code.interfaces.branch import IBranch
>  from lp.code.interfaces.branchmergeproposal import IBranchMergeProposal
>  from lp.code.interfaces.codereviewcomment import ICodeReviewComment
>  from lp.code.interfaces.codereviewinlinecomment import (
> @@ -355,7 +356,7 @@
>      @property
>      def codebrowse_url(self):
>          """Return the link to codebrowse for this branch."""
> -        return self.context.source_branch.getCodebrowseUrl()
> +        return self.context.merge_source.getCodebrowseUrl()
>  
>  
>  class BranchMergeProposalRevisionIdMixin:
> @@ -373,6 +374,9 @@
>              return None
>          # If the source branch is REMOTE, then there won't be any ids.
>          source_branch = self.context.source_branch
> +        if source_branch is None:
> +            # Git doesn't have revision numbers.  Just use the ids.
> +            return revision_id
>          if source_branch.branch_type == BranchType.REMOTE:
>              return revision_id
>          else:
> @@ -586,12 +590,18 @@
>      def initialize(self):
>          super(BranchMergeProposalView, self).initialize()
>          cache = IJSONRequestCache(self.request)
> -        cache.objects.update({
> -            'branch_diff_link':
> -                'https://%s/+loggerhead/%s/diff/' % (
> -                    config.launchpad.code_domain,
> -                    self.context.source_branch.unique_name),
> -            })
> +        if IBranch.providedBy(self.context.merge_source):
> +            cache.objects.update({
> +                'branch_diff_link':
> +                    'https://%s/+loggerhead/%s/diff/' % (
> +                        config.launchpad.code_domain,
> +                        self.context.source_branch.unique_name),
> +                })
> +        else:
> +            cache.objects.update({
> +                'repository_diff_link':
> +                    '%s/diff/' % self.context.merge_source.getCodebrowseUrl(),
> +                })

This isn't useful, as the Same Origin Policy will forbid the request. I'd consider just not implementing the (disabled for all but ~launchpad) feature for Git yet.

>          if getFeatureFlag("longpoll.merge_proposals.enabled"):
>              cache.objects['merge_proposal_event_key'] = subscribe(
>                  self.context).event_key
> @@ -618,7 +628,9 @@
>          # Sort the comments by date order.
>          merge_proposal = self.context
>          groups = list(merge_proposal.getRevisionsSinceReviewStart())
> -        source = DecoratedBranch(merge_proposal.source_branch)
> +        source = merge_proposal.merge_source
> +        if IBranch.providedBy(source):
> +            source = DecoratedBranch(source)
>          comments = []
>          if getFeatureFlag('code.incremental_diffs.enabled'):
>              ranges = [
> @@ -685,6 +697,8 @@
>  
>      @property
>      def pending_diff(self):
> +        if self.context.source_branch is None:
> +            return False
>          return (
>              self.context.next_preview_diff_job is not None or
>              self.context.source_branch.pending_writes)
> @@ -705,6 +719,10 @@
>  
>      @property
>      def spec_links(self):
> +        if self.context.source_branch is None:
> +            # XXX cjwatson 2015-04-16: Implement once Git refs have linked
> +            # blueprints.
> +            return []
>          return list(
>              self.context.source_branch.getSpecificationLinks(self.user))
>  
> @@ -748,12 +766,16 @@
>      @property
>      def status_config(self):
>          """The config to configure the ChoiceSource JS widget."""
> +        if IBranch.providedBy(self.context.merge_source):
> +            source_revid = self.context.merge_source.last_scanned_id
> +        else:
> +            source_revid = self.context.merge_source.commit_sha1
>          return simplejson.dumps({
>              'status_widget_items': vocabulary_to_choice_edit_items(
>                  self._createStatusVocabulary(),
>                  css_class_prefix='mergestatus'),
>              'status_value': self.context.queue_status.title,
> -            'source_revid': self.context.source_branch.last_scanned_id,
> +            'source_revid': source_revid,
>              'user_can_edit_status': check_permission(
>                  'launchpad.Edit', self.context),
>              })
> @@ -965,19 +987,35 @@
>      schema = ResubmitSchema
>      for_input = True
>      page_title = label = "Resubmit proposal to merge"
> -    field_names = [
> -        'source_branch',
> -        'target_branch',
> -        'prerequisite_branch',
> -        'description',
> -        'break_link',
> -        ]
>  
>      def initialize(self):
>          self.cancel_url = canonical_url(self.context)
>          super(BranchMergeProposalResubmitView, self).initialize()
>  
>      @property
> +    def field_names(self):
> +        if IBranch.providedBy(self.context.merge_source):
> +            field_names = [
> +                'source_branch',
> +                'target_branch',
> +                'prerequisite_branch',
> +                ]
> +        else:
> +            field_names = [
> +                'source_git_repository',
> +                'source_git_path',
> +                'target_git_repository',
> +                'target_git_path',
> +                'prerequisite_git_repository',
> +                'prerequisite_git_path',
> +                ]
> +        field_names.extend([
> +            'description',
> +            'break_link',
> +            ])
> +        return field_names
> +
> +    @property
>      def initial_values(self):
>          UNSET = object()
>          items = ((key, getattr(self.context, key, UNSET)) for key in
> @@ -988,10 +1026,24 @@
>      def resubmit_action(self, action, data):
>          """Resubmit this proposal."""
>          try:
> +            if IBranch.providedBy(self.context.merge_source):
> +                merge_source = data['source_branch']
> +                merge_target = data['target_branch']
> +                merge_prerequisite = data['prerequisite_branch']
> +            else:
> +                merge_source = data['source_git_repository'].getRefByPath(
> +                    data['source_git_path'])
> +                merge_target = data['target_git_repository'].getRefByPath(
> +                    data['target_git_path'])
> +                if data['prerequisite_git_repository']:
> +                    merge_prerequisite = (
> +                        data['prerequisite_git_repository'].getRefByPath(
> +                            data['prerequisite_git_path']))
> +                else:
> +                    merge_prerequisite = None
>              proposal = self.context.resubmit(
> -                self.user, data['source_branch'], data['target_branch'],
> -                data['prerequisite_branch'], data['description'],
> -                data['break_link'])
> +                self.user, merge_source, merge_target, merge_prerequisite,
> +                data['description'], data['break_link'])
>          except BranchMergeProposalExists as e:
>              message = structured(
>                  'Cannot resubmit because <a href="%(url)s">a similar merge'
> @@ -1072,18 +1124,31 @@
>      """The view to mark a merge proposal as merged."""
>      schema = IBranchMergeProposal
>      page_title = label = "Edit branch merge proposal"
> -    field_names = ["merged_revno"]
>      for_input = True
>  
>      @property
> +    def field_names(self):
> +        if IBranch.providedBy(self.context.merge_target):
> +            return ["merged_revno"]
> +        else:
> +            return ["merged_revision_id"]
> +
> +    @property
>      def initial_values(self):
>          # Default to the tip of the target branch, on the assumption that the
>          # source branch has just been merged into it.
> -        if self.context.merged_revno is not None:
> -            revno = self.context.merged_revno
> +        if IBranch.providedBy(self.context.merge_target):
> +            if self.context.merged_revno is not None:
> +                revno = self.context.merged_revno
> +            else:
> +                revno = self.context.merge_target.revision_count
> +            return {'merged_revno': revno}
>          else:
> -            revno = self.context.target_branch.revision_count
> -        return {'merged_revno': revno}
> +            if self.context.merged_revision_id is not None:
> +                revision_id = self.context.merged_revision_id
> +            else:
> +                revision_id = self.context.merge_target.commit_sha1
> +            return {'merged_revision_id': revision_id}
>  
>      @property
>      def next_url(self):
> @@ -1095,13 +1160,16 @@
>      @notify
>      def mark_merged_action(self, action, data):
>          """Update the whiteboard and go back to the source branch."""
> -        revno = data['merged_revno']
> +        if IBranch.providedBy(self.context.merge_target):
> +            kwargs = {'merged_revno': data['merged_revno']}
> +        else:
> +            kwargs = {'merged_revision_id': data['merged_revision_id']}
>          if self.context.queue_status == BranchMergeProposalStatus.MERGED:
> -            self.context.markAsMerged(merged_revno=revno)
> +            self.context.markAsMerged(**kwargs)
>              self.request.response.addNotification(
>                  'The proposal\'s merged revision has been updated.')
>          else:
> -            self.context.markAsMerged(revno, merge_reporter=self.user)
> +            self.context.markAsMerged(merge_reporter=self.user, **kwargs)
>              self.request.response.addNotification(
>                  'The proposal has now been marked as merged.')
>  
> 
> === modified file 'lib/lp/code/browser/configure.zcml'
> --- lib/lp/code/browser/configure.zcml	2015-04-22 16:11:40 +0000
> +++ lib/lp/code/browser/configure.zcml	2015-04-28 16:48:46 +0000
> @@ -824,6 +824,9 @@
>          <browser:page
>              name="++ref-commits"
>              template="../templates/gitref-commits.pt"/>
> +        <browser:page
> +            name="++ref-pending-merges"
> +            template="../templates/gitref-pending-merges.pt"/>
>      </browser:pages>
>      <browser:page
>          for="lp.code.interfaces.gitref.IGitRef"
> 
> === modified file 'lib/lp/code/browser/gitref.py'
> --- lib/lp/code/browser/gitref.py	2015-04-22 16:11:40 +0000
> +++ lib/lp/code/browser/gitref.py	2015-04-28 16:48:46 +0000
> @@ -10,12 +10,19 @@
>      'GitRefView',
>      ]
>  
> +from lp.code.browser.branchmergeproposal import (
> +    latest_proposals_for_each_branch,
> +    )
>  from lp.code.interfaces.gitref import IGitRef
> +from lp.code.model.gitrepository import GitRepository
> +from lp.services.database.bulk import load_related
> +from lp.services.propertycache import cachedproperty
>  from lp.services.webapp import (
>      LaunchpadView,
>      Navigation,
>      stepthrough,
>      )
> +from lp.services.webapp.authorization import check_permission
>  
>  
>  class GitRefNavigation(Navigation):
> @@ -49,3 +56,57 @@
>              "author_date": self.context.author_date,
>              "commit_message": self.context.commit_message,
>              }
> +
> +    @property
> +    def show_merge_links(self):
> +        """Return whether or not merge proposal links should be shown.
> +
> +        Merge proposal links should not be shown if there is only one
> +        reference in the entire target.
> +        """
> +        if not self.context.namespace.supports_merge_proposals:
> +            return False
> +        repositories = self.context.namespace.collection.getRepositories()
> +        if repositories.count() > 1:
> +            return True
> +        repository = repositories.one()
> +        if repository is None:
> +            return False
> +        return repository.refs.count() > 1
> +
> +    @cachedproperty
> +    def landing_targets(self):
> +        """Return a filtered list of landing targets."""
> +        return latest_proposals_for_each_branch(self.context.landing_targets)
> +
> +    @cachedproperty
> +    def landing_candidates(self):
> +        """Return a decorated list of landing candidates."""
> +        candidates = list(self.context.landing_candidates)
> +        load_related(
> +            GitRepository, candidates,
> +            ["source_git_repositoryID", "prerequisite_git_repositoryID"])
> +        return [proposal for proposal in candidates
> +                if check_permission("launchpad.View", proposal)]
> +
> +    def _getBranchCountText(self, count):
> +        """Help to show user friendly text."""
> +        if count == 0:
> +            return 'No branches'
> +        elif count == 1:
> +            return '1 branch'
> +        else:
> +            return '%s branches' % count
> +
> +    @cachedproperty
> +    def landing_candidate_count_text(self):
> +        return self._getBranchCountText(len(self.landing_candidates))
> +
> +    @cachedproperty
> +    def dependent_landings(self):
> +        return [proposal for proposal in self.context.dependent_landings
> +                if check_permission("launchpad.View", proposal)]
> +
> +    @cachedproperty
> +    def dependent_landing_count_text(self):
> +        return self._getBranchCountText(len(self.dependent_landings))
> 
> === modified file 'lib/lp/code/browser/tests/test_branchmergeproposal.py'
> --- lib/lp/code/browser/tests/test_branchmergeproposal.py	2015-04-19 12:56:32 +0000
> +++ lib/lp/code/browser/tests/test_branchmergeproposal.py	2015-04-28 16:48:46 +0000
> @@ -46,6 +46,7 @@
>      BranchMergeProposalStatus,
>      CodeReviewVote,
>      )
> +from lp.code.interfaces.gitrepository import GIT_FEATURE_FLAG
>  from lp.code.model.diff import PreviewDiff
>  from lp.code.tests.helpers import (
>      add_revision_to_branch,
> @@ -55,6 +56,7 @@
>      PersonVisibility,
>      TeamMembershipPolicy,
>      )
> +from lp.services.features.testing import FeatureFixture
>  from lp.services.librarian.interfaces.client import LibrarianServerError
>  from lp.services.messages.model.message import MessageSet
>  from lp.services.webapp import canonical_url
> @@ -107,8 +109,8 @@
>          self.assertTrue(d.can_change_review)
>  
>  
> -class TestBranchMergeProposalMergedView(TestCaseWithFactory):
> -    """Tests for `BranchMergeProposalMergedView`."""
> +class TestBranchMergeProposalMergedViewBzr(TestCaseWithFactory):
> +    """Tests for `BranchMergeProposalMergedView` for Bazaar."""
>  
>      layer = DatabaseFunctionalLayer
>  
> @@ -129,6 +131,30 @@
>              view.initial_values)
>  
>  
> +class TestBranchMergeProposalMergedViewGit(TestCaseWithFactory):
> +    """Tests for `BranchMergeProposalMergedView` for Git."""
> +
> +    layer = DatabaseFunctionalLayer
> +
> +    def setUp(self):
> +        # Use an admin so we don't have to worry about launchpad.Edit
> +        # permissions on the merge proposals for adding comments, or
> +        # nominating reviewers.
> +        TestCaseWithFactory.setUp(self, user="admin@xxxxxxxxxxxxx")
> +        self.useFixture(FeatureFixture({GIT_FEATURE_FLAG: u"on"}))
> +        self.bmp = self.factory.makeBranchMergeProposalForGit()
> +
> +    def test_initial_values(self):
> +        # The default merged_revision_id is the head commit SHA-1 of the
> +        # target ref.
> +        view = BranchMergeProposalMergedView(self.bmp, LaunchpadTestRequest())
> +        removeSecurityProxy(self.bmp.source_git_ref).commit_sha1 = "0" * 40
> +        removeSecurityProxy(self.bmp.target_git_ref).commit_sha1 = "1" * 40
> +        self.assertEqual(
> +            {'merged_revision_id': self.bmp.target_git_ref.commit_sha1},
> +            view.initial_values)
> +
> +
>  class TestBranchMergeProposalAddVoteView(TestCaseWithFactory):
>      """Test the AddVote view."""
>  
> @@ -661,14 +687,14 @@
>          self.assertEqual(BrowserNotificationLevel.INFO, notification.level)
>  
>  
> -class TestBranchMergeProposalResubmitView(TestCaseWithFactory):
> +class TestBranchMergeProposalResubmitViewMixin:
>      """Test BranchMergeProposalResubmitView."""
>  
>      layer = DatabaseFunctionalLayer
>  
>      def createView(self):
>          """Create the required view."""
> -        context = self.factory.makeBranchMergeProposal()
> +        context = self._makeBranchMergeProposal()
>          self.useContext(person_logged_in(context.registrant))
>          view = BranchMergeProposalResubmitView(
>              context, LaunchpadTestRequest())
> @@ -679,36 +705,34 @@
>          """resubmit_action resubmits the proposal."""
>          view = self.createView()
>          context = view.context
> -        new_proposal = view.resubmit_action.success(
> -            {'source_branch': context.source_branch,
> -             'target_branch': context.target_branch,
> -             'prerequisite_branch': context.prerequisite_branch,
> -             'description': None,
> -             'break_link': False,
> -            })
> +        new_proposal = view.resubmit_action.success(self._getFormValues(
> +            context.merge_source, context.merge_target,
> +            context.merge_prerequisite, {
> +                'description': None,
> +                'break_link': False,
> +                }))
>          self.assertEqual(new_proposal.supersedes, context)
> -        self.assertEqual(new_proposal.source_branch, context.source_branch)
> -        self.assertEqual(new_proposal.target_branch, context.target_branch)
> +        self.assertEqual(new_proposal.merge_source, context.merge_source)
> +        self.assertEqual(new_proposal.merge_target, context.merge_target)
>          self.assertEqual(
> -            new_proposal.prerequisite_branch, context.prerequisite_branch)
> +            new_proposal.merge_prerequisite, context.merge_prerequisite)
>  
>      def test_resubmit_action_change_branches(self):
>          """Changing the branches changes the branches in the new proposal."""
>          view = self.createView()
> -        target = view.context.source_branch.target
> -        new_source = self.factory.makeBranchTargetBranch(target)
> -        new_target = self.factory.makeBranchTargetBranch(target)
> -        new_prerequisite = self.factory.makeBranchTargetBranch(target)
> -        new_proposal = view.resubmit_action.success(
> -            {'source_branch': new_source, 'target_branch': new_target,
> -             'prerequisite_branch': new_prerequisite,
> -             'description': 'description',
> -             'break_link': False,
> -             })
> +        new_source = self._makeBranchWithSameTarget(view.context.merge_source)
> +        new_target = self._makeBranchWithSameTarget(view.context.merge_source)
> +        new_prerequisite = self._makeBranchWithSameTarget(
> +            view.context.merge_source)
> +        new_proposal = view.resubmit_action.success(self._getFormValues(
> +            new_source, new_target, new_prerequisite, {
> +                'description': 'description',
> +                'break_link': False,
> +                }))
>          self.assertEqual(new_proposal.supersedes, view.context)
> -        self.assertEqual(new_proposal.source_branch, new_source)
> -        self.assertEqual(new_proposal.target_branch, new_target)
> -        self.assertEqual(new_proposal.prerequisite_branch, new_prerequisite)
> +        self.assertEqual(new_proposal.merge_source, new_source)
> +        self.assertEqual(new_proposal.merge_target, new_target)
> +        self.assertEqual(new_proposal.merge_prerequisite, new_prerequisite)
>  
>      def test_resubmit_action_break_link(self):
>          """Enabling break_link prevents linking the old and new proposals."""
> @@ -716,24 +740,22 @@
>          new_proposal = self.resubmitDefault(view, break_link=True)
>          self.assertIs(None, new_proposal.supersedes)
>  
> -    @staticmethod
> -    def resubmitDefault(view, break_link=False, prerequisite_branch=None):
> +    @classmethod
> +    def resubmitDefault(cls, view, break_link=False, merge_prerequisite=None):
>          context = view.context
> -        if prerequisite_branch is None:
> -            prerequisite_branch = context.prerequisite_branch
> -        return view.resubmit_action.success(
> -            {'source_branch': context.source_branch,
> -             'target_branch': context.target_branch,
> -             'prerequisite_branch': prerequisite_branch,
> -             'description': None,
> -             'break_link': break_link,
> -            })
> +        if merge_prerequisite is None:
> +            merge_prerequisite = context.merge_prerequisite
> +        return view.resubmit_action.success(cls._getFormValues(
> +            context.merge_source, context.merge_target, merge_prerequisite, {
> +                'description': None,
> +                'break_link': break_link,
> +                }))
>  
>      def test_resubmit_existing(self):
>          """Resubmitting a proposal when another is active is a user error."""
>          view = self.createView()
>          first_bmp = view.context
> -        with person_logged_in(first_bmp.target_branch.owner):
> +        with person_logged_in(first_bmp.merge_target.owner):
>              first_bmp.resubmit(first_bmp.registrant)
>          self.resubmitDefault(view)
>          (notification,) = view.request.response.notifications
> @@ -742,19 +764,86 @@
>              ' <a href=.*>a similar merge proposal</a> is already active.'))
>          self.assertEqual(BrowserNotificationLevel.ERROR, notification.level)
>  
> +
> +class TestBranchMergeProposalResubmitViewBzr(
> +    TestBranchMergeProposalResubmitViewMixin, TestCaseWithFactory):
> +    """Test BranchMergeProposalResubmitView for Bazaar."""
> +
> +    def _makeBranchMergeProposal(self):
> +        return self.factory.makeBranchMergeProposal()
> +
> +    @staticmethod
> +    def _getFormValues(source_branch, target_branch, prerequisite_branch,
> +                       extras):
> +        values = {
> +            'source_branch': source_branch,
> +            'target_branch': target_branch,
> +            'prerequisite_branch': prerequisite_branch,
> +            }
> +        values.update(extras)
> +        return values
> +
> +    def _makeBranchWithSameTarget(self, branch):
> +        return self.factory.makeBranchTargetBranch(branch.target)
> +
>      def test_resubmit_same_target_prerequisite(self):
>          """User error if same branch is target and prerequisite."""
>          view = self.createView()
>          first_bmp = view.context
> -        self.resubmitDefault(
> -            view, prerequisite_branch=first_bmp.target_branch)
> +        self.resubmitDefault(view, merge_prerequisite=first_bmp.merge_target)
>          self.assertEqual(
>              view.errors,
>              ['Target and prerequisite branches must be different.'])
>  
>  
> -class TestResubmitBrowser(BrowserTestCase):
> -    """Browser tests for resubmitting branch merge proposals."""
> +class TestBranchMergeProposalResubmitViewGit(
> +    TestBranchMergeProposalResubmitViewMixin, TestCaseWithFactory):
> +    """Test BranchMergeProposalResubmitView for Git."""
> +
> +    def setUp(self):
> +        super(TestBranchMergeProposalResubmitViewGit, self).setUp()
> +        self.useFixture(FeatureFixture({GIT_FEATURE_FLAG: u"on"}))
> +
> +    def _makeBranchMergeProposal(self):
> +        return self.factory.makeBranchMergeProposalForGit()
> +
> +    @staticmethod
> +    def _getFormValues(source_branch, target_branch, prerequisite_branch,
> +                       extras):
> +        values = {
> +            'source_git_repository': source_branch.repository,
> +            'source_git_path': source_branch.path,
> +            'target_git_repository': target_branch.repository,
> +            'target_git_path': target_branch.path,
> +            }
> +        if prerequisite_branch is not None:
> +            values.update({
> +                'prerequisite_git_repository': prerequisite_branch.repository,
> +                'prerequisite_git_path': prerequisite_branch.path,
> +                })
> +        else:
> +            values.update({
> +                'prerequisite_git_repository': None,
> +                'prerequisite_git_path': '',
> +                })
> +        values.update(extras)
> +        return values
> +
> +    def _makeBranchWithSameTarget(self, branch):
> +        return self.factory.makeGitRefs(target=branch.target)[0]
> +
> +    def test_resubmit_same_target_prerequisite(self):
> +        """User error if same branch is target and prerequisite."""
> +        view = self.createView()
> +        first_bmp = view.context
> +        self.resubmitDefault(view, merge_prerequisite=first_bmp.merge_target)
> +        self.assertEqual(
> +            view.errors,
> +            ['Target and prerequisite references must be different.'])
> +
> +
> +class TestResubmitBrowserBzr(BrowserTestCase):
> +    """Browser tests for resubmitting branch merge proposals for Bazaar."""
>  
>      layer = DatabaseFunctionalLayer
>  
> @@ -781,6 +870,41 @@
>              self.assertEqual('flibble', bmp.superseded_by.description)
>  
>  
> +class TestResubmitBrowserGit(BrowserTestCase):
> +    """Browser tests for resubmitting branch merge proposals for Git."""
> +
> +    layer = DatabaseFunctionalLayer
> +
> +    def setUp(self):
> +        super(TestResubmitBrowserGit, self).setUp()
> +        self.useFixture(FeatureFixture({GIT_FEATURE_FLAG: u"on"}))
> +
> +    def test_resubmit_text(self):
> +        """The text of the resubmit page is as expected."""
> +        bmp = self.factory.makeBranchMergeProposalForGit(registrant=self.user)
> +        text = self.getMainText(bmp, '+resubmit')
> +        expected = (
> +            'Resubmit proposal to merge.*'
> +            'Source Git Repository:.*'
> +            'Source Git branch path:.*'
> +            'Target Git Repository:.*'
> +            'Target Git branch path:.*'
> +            'Prerequisite Git Repository:.*'
> +            'Prerequisite Git branch path:.*'
> +            'Description.*'
> +            'Start afresh.*')
> +        self.assertTextMatchesExpressionIgnoreWhitespace(expected, text)
> +
> +    def test_resubmit_controls(self):
> +        """Proposals can be resubmitted using the browser."""
> +        bmp = self.factory.makeBranchMergeProposalForGit(registrant=self.user)
> +        browser = self.getViewBrowser(bmp, '+resubmit')
> +        browser.getControl('Description').value = 'flibble'
> +        browser.getControl('Resubmit').click()
> +        with person_logged_in(self.user):
> +            self.assertEqual('flibble', bmp.superseded_by.description)
> +
> +
>  class TestBranchMergeProposalView(TestCaseWithFactory):
>  
>      layer = LaunchpadFunctionalLayer
> @@ -1248,17 +1372,17 @@
>          self.assertEqual(mp_url, browser.url)
>  
>  
> -class TestLatestProposalsForEachBranch(TestCaseWithFactory):
> +class TestLatestProposalsForEachBranchMixin:
>      """Confirm that the latest branch is returned."""
>  
>      layer = DatabaseFunctionalLayer
>  
>      def test_newest_first(self):
>          # If each proposal targets a different branch, each will be returned.
> -        bmp1 = self.factory.makeBranchMergeProposal(
> +        bmp1 = self._makeBranchMergeProposal(
>              date_created=(
>                  datetime(year=2008, month=9, day=10, tzinfo=pytz.UTC)))
> -        bmp2 = self.factory.makeBranchMergeProposal(
> +        bmp2 = self._makeBranchMergeProposal(
>              date_created=(
>                  datetime(year=2008, month=10, day=10, tzinfo=pytz.UTC)))
>          self.assertEqual(
> @@ -1266,27 +1390,57 @@
>  
>      def test_visible_filtered_out(self):
>          # If the proposal is not visible to the user, they are not returned.
> -        bmp1 = self.factory.makeBranchMergeProposal(
> +        bmp1 = self._makeBranchMergeProposal(
>              date_created=(
>                  datetime(year=2008, month=9, day=10, tzinfo=pytz.UTC)))
> -        bmp2 = self.factory.makeBranchMergeProposal(
> +        bmp2 = self._makeBranchMergeProposal(
>              date_created=(
>                  datetime(year=2008, month=10, day=10, tzinfo=pytz.UTC)))
> -        removeSecurityProxy(bmp2.source_branch).transitionToInformationType(
> -            InformationType.USERDATA, bmp2.source_branch.owner,
> -            verify_policy=False)
> +        self._setBranchInvisible(bmp2.merge_source)
>          self.assertEqual(
>              [bmp1], latest_proposals_for_each_branch([bmp1, bmp2]))
>  
>      def test_same_target(self):
>          # If the proposals target the same branch, then the most recent is
>          # returned.
> -        bmp1 = self.factory.makeBranchMergeProposal(
> +        bmp1 = self._makeBranchMergeProposal(
>              date_created=(
>                  datetime(year=2008, month=9, day=10, tzinfo=pytz.UTC)))
> -        bmp2 = self.factory.makeBranchMergeProposal(
> -            target_branch=bmp1.target_branch,
> +        bmp2 = self._makeBranchMergeProposal(
> +            merge_target=bmp1.merge_target,
>              date_created=(
>                  datetime(year=2008, month=10, day=10, tzinfo=pytz.UTC)))
>          self.assertEqual(
>              [bmp2], latest_proposals_for_each_branch([bmp1, bmp2]))
> +
> +
> +class TestLatestProposalsForEachBranchBzr(
> +    TestLatestProposalsForEachBranchMixin, TestCaseWithFactory):
> +    """Confirm that the latest branch is returned for Bazaar."""
> +
> +    def _makeBranchMergeProposal(self, merge_target=None, **kwargs):
> +        return self.factory.makeBranchMergeProposal(
> +            target_branch=merge_target, **kwargs)
> +
> +    @staticmethod
> +    def _setBranchInvisible(branch):
> +        removeSecurityProxy(branch).transitionToInformationType(
> +            InformationType.USERDATA, branch.owner, verify_policy=False)
> +
> +
> +class TestLatestProposalsForEachBranchGit(
> +    TestLatestProposalsForEachBranchMixin, TestCaseWithFactory):
> +    """Confirm that the latest branch is returned for Bazaar."""
> +
> +    def setUp(self):
> +        super(TestLatestProposalsForEachBranchGit, self).setUp()
> +        self.useFixture(FeatureFixture({GIT_FEATURE_FLAG: u"on"}))
> +
> +    def _makeBranchMergeProposal(self, merge_target=None, **kwargs):
> +        return self.factory.makeBranchMergeProposalForGit(
> +            target_ref=merge_target, **kwargs)
> +
> +    @staticmethod
> +    def _setBranchInvisible(branch):
> +        removeSecurityProxy(branch.repository).transitionToInformationType(
> +            InformationType.USERDATA, branch.owner, verify_policy=False)
> 
> === modified file 'lib/lp/code/errors.py'
> --- lib/lp/code/errors.py	2015-04-19 12:56:32 +0000
> +++ lib/lp/code/errors.py	2015-04-28 16:48:46 +0000
> @@ -231,11 +231,18 @@
>      """Raised if there is already a matching BranchMergeProposal."""
>  
>      def __init__(self, existing_proposal):
> +        # Circular import.
> +        from lp.code.interfaces.branch import IBranch
> +        # display_name is the newer style, but IBranch uses the older style.
> +        if IBranch.providedBy(existing_proposal.merge_source):
> +            display_name = "displayname"
> +        else:
> +            display_name = "display_name"
>          super(BranchMergeProposalExists, self).__init__(
>                  'There is already a branch merge proposal registered for '
>                  'branch %s to land on %s that is still active.' %
> -                (existing_proposal.source_branch.displayname,
> -                 existing_proposal.target_branch.displayname))
> +                (getattr(existing_proposal.merge_source, display_name),
> +                 getattr(existing_proposal.merge_target, display_name)))
>          self.existing_proposal = existing_proposal
>  
>  
> 
> === modified file 'lib/lp/code/interfaces/gitrepository.py'
> --- lib/lp/code/interfaces/gitrepository.py	2015-04-22 16:11:40 +0000
> +++ lib/lp/code/interfaces/gitrepository.py	2015-04-28 16:48:46 +0000
> @@ -49,6 +49,7 @@
>      Choice,
>      Datetime,
>      Int,
> +    List,
>      Text,
>      TextLine,
>      )
> @@ -607,6 +608,39 @@
>          :return: A collection of `IGitRepository` objects.
>          """
>  
> +    @call_with(user=REQUEST_USER)
> +    @operation_parameters(
> +        person=Reference(
> +            title=_("The person whose repository visibility is being "
> +                    "checked."),
> +            schema=IPerson),
> +        repository_names=List(value_type=Text(),
> +            title=_('List of repository unique names'), required=True),
> +    )
> +    @export_read_operation()
> +    @operation_for_version("devel")
> +    def getRepositoryVisibilityInfo(user, person, repository_names):
> +        """Return the named repositories visible to both user and person.
> +
> +        Anonymous requesters don't get any information.
> +
> +        :param user: The user requesting the information. If the user is
> +            None then we return an empty dict.
> +        :param person: The person whose repository visibility we wish to
> +            check.
> +        :param repository_names: The unique names of the repositories to
> +            check.
> +
> +        Return a dict with the following values:
> +        person_name: the displayname of the person.
> +        visible_repositories: a list of the unique names of the repositories
> +        which the requester and specified person can both see.
> +
> +        This API call is provided for use by the client Javascript.  It is
> +        not designed to efficiently scale to handle requests for large
> +        numbers of repositories.
> +        """
> +
>      @operation_parameters(
>          target=Reference(
>              title=_("Target"), required=True, schema=IHasGitRepositories))
> 
> === modified file 'lib/lp/code/model/gitref.py'
> --- lib/lp/code/model/gitref.py	2015-04-24 12:58:46 +0000
> +++ lib/lp/code/model/gitref.py	2015-04-28 16:48:46 +0000
> @@ -376,3 +376,6 @@
>  
>      def __ne__(self, other):
>          return not self == other
> +
> +    def __hash__(self):
> +        return hash(self.repository) ^ hash(self.path) ^ hash(self.commit_sha1)
> 
> === modified file 'lib/lp/code/model/gitrepository.py'
> --- lib/lp/code/model/gitrepository.py	2015-04-22 16:42:57 +0000
> +++ lib/lp/code/model/gitrepository.py	2015-04-28 16:48:46 +0000
> @@ -38,6 +38,7 @@
>  from storm.store import Store
>  from zope.component import getUtility
>  from zope.interface import implements
> +from zope.security.interfaces import Unauthorized
>  from zope.security.proxy import removeSecurityProxy
>  
>  from lp.app.enums import (
> @@ -774,6 +775,27 @@
>          collection = IGitCollection(target).visibleByUser(user)
>          return collection.getRepositories(eager_load=True)
>  
> +    def getRepositoryVisibilityInfo(self, user, person, repository_names):
> +        """See `IGitRepositorySet`."""
> +        if user is None:
> +            return dict()
> +        lookup = getUtility(IGitLookup)
> +        visible_repositories = []
> +        for name in repository_names:
> +            repository = lookup.getByUniqueName(name)
> +            try:
> +                if (repository is not None
> +                        and repository.visibleByUser(user)
> +                        and repository.visibleByUser(person)):
> +                    visible_repositories.append(repository.unique_name)
> +            except Unauthorized:
> +                # We don't include repositories user cannot see.
> +                pass
> +        return {
> +            'person_name': person.displayname,
> +            'visible_repositories': visible_repositories,
> +            }
> +
>      def getDefaultRepository(self, target):
>          """See `IGitRepositorySet`."""
>          clauses = [GitRepository.target_default == True]
> 
> === modified file 'lib/lp/code/model/tests/test_gitrepository.py'
> --- lib/lp/code/model/tests/test_gitrepository.py	2015-04-21 23:05:48 +0000
> +++ lib/lp/code/model/tests/test_gitrepository.py	2015-04-28 16:48:46 +0000
> @@ -1216,6 +1216,80 @@
>              public_repositories + [private_repository],
>              self.repository_set.getRepositories(other_person, project))
>  
> +    def test_getRepositoryVisibilityInfo_empty_repository_names(self):
> +        # If repository_names is empty, getRepositoryVisibilityInfo returns
> +        # an empty visible_repositories list.
> +        person = self.factory.makePerson(name="fred")
> +        info = self.repository_set.getRepositoryVisibilityInfo(
> +            person, person, repository_names=[])
> +        self.assertEqual("Fred", info["person_name"])
> +        self.assertEqual([], info["visible_repositories"])
> +
> +    def test_getRepositoryVisibilityInfo(self):
> +        person = self.factory.makePerson(name="fred")
> +        owner = self.factory.makePerson()
> +        visible_repository = self.factory.makeGitRepository()
> +        invisible_repository = self.factory.makeGitRepository(
> +            owner=owner, information_type=InformationType.USERDATA)
> +        invisible_name = removeSecurityProxy(invisible_repository).unique_name
> +        repositories = [visible_repository.unique_name, invisible_name]
> +
> +        with person_logged_in(owner):
> +            info = self.repository_set.getRepositoryVisibilityInfo(
> +                owner, person, repository_names=repositories)
> +        self.assertEqual("Fred", info["person_name"])
> +        self.assertEqual(
> +            [visible_repository.unique_name], info["visible_repositories"])
> +
> +    def test_getRepositoryVisibilityInfo_unauthorised_user(self):
> +        # If the user making the API request cannot see one of the
> +        # repositories, that repository is not included in the results.
> +        person = self.factory.makePerson(name="fred")
> +        owner = self.factory.makePerson()
> +        visible_repository = self.factory.makeGitRepository()
> +        invisible_repository = self.factory.makeGitRepository(
> +            owner=owner, information_type=InformationType.USERDATA)
> +        invisible_name = removeSecurityProxy(invisible_repository).unique_name
> +        repositories = [visible_repository.unique_name, invisible_name]
> +
> +        someone = self.factory.makePerson()
> +        with person_logged_in(someone):
> +            info = self.repository_set.getRepositoryVisibilityInfo(
> +                someone, person, repository_names=repositories)
> +        self.assertEqual("Fred", info["person_name"])
> +        self.assertEqual(
> +            [visible_repository.unique_name], info["visible_repositories"])
> +
> +    def test_getRepositoryVisibilityInfo_anonymous(self):
> +        # Anonymous users are not allowed to see any repository visibility
> +        # information, even if the repository they are querying about is
> +        # public.
> +        person = self.factory.makePerson(name="fred")
> +        owner = self.factory.makePerson()
> +        visible_repository = self.factory.makeGitRepository(owner=owner)
> +        repositories = [visible_repository.unique_name]
> +
> +        with person_logged_in(owner):
> +            info = self.repository_set.getRepositoryVisibilityInfo(
> +                None, person, repository_names=repositories)
> +        self.assertEqual({}, info)
> +
> +    def test_getRepositoryVisibilityInfo_invalid_repository_name(self):
> +        # If an invalid repository name is specified, it is not included.
> +        person = self.factory.makePerson(name="fred")
> +        owner = self.factory.makePerson()
> +        visible_repository = self.factory.makeGitRepository(owner=owner)
> +        repositories = [
> +            visible_repository.unique_name,
> +            "invalid_repository_name"]
> +
> +        with person_logged_in(owner):
> +            info = self.repository_set.getRepositoryVisibilityInfo(
> +                owner, person, repository_names=repositories)
> +        self.assertEqual("Fred", info["person_name"])
> +        self.assertEqual(
> +            [visible_repository.unique_name], info["visible_repositories"])
> +
>      def test_setDefaultRepository_refuses_person(self):
>          # setDefaultRepository refuses if the target is a person.
>          person = self.factory.makePerson()
> 
> === modified file 'lib/lp/code/templates/branchmergeproposal-resubmit.pt'
> --- lib/lp/code/templates/branchmergeproposal-resubmit.pt	2010-11-04 16:56:35 +0000
> +++ lib/lp/code/templates/branchmergeproposal-resubmit.pt	2015-04-28 16:48:46 +0000
> @@ -24,7 +24,7 @@
>      </div>
>    </div>
>  
> -  <div id="source-revisions">
> +  <div id="source-revisions" tal:condition="context/source_branch">
>      <tal:history-available condition="context/source_branch/revision_count"
>                             define="branch context/source_branch;
>                                     revisions view/unlanded_revisions">
> 
> === modified file 'lib/lp/code/templates/gitref-index.pt'
> --- lib/lp/code/templates/gitref-index.pt	2015-03-19 17:04:22 +0000
> +++ lib/lp/code/templates/gitref-index.pt	2015-04-28 16:48:46 +0000
> @@ -17,6 +17,13 @@
>  <div metal:fill-slot="main">
>  
>    <div class="yui-g">
> +    <div id="ref-relations" class="portlet">
> +      <tal:ref-pending-merges
> +           replace="structure context/@@++ref-pending-merges" />
> +    </div>
> +  </div>
> +
> +  <div class="yui-g">
>      <div id="ref-info" class="portlet">
>        <h2>Branch information</h2>
>        <div class="two-column-list">
> 
> === added file 'lib/lp/code/templates/gitref-pending-merges.pt'
> --- lib/lp/code/templates/gitref-pending-merges.pt	1970-01-01 00:00:00 +0000
> +++ lib/lp/code/templates/gitref-pending-merges.pt	2015-04-28 16:48:46 +0000
> @@ -0,0 +1,43 @@
> +<div
> +  xmlns:tal="http://xml.zope.org/namespaces/tal";
> +  xmlns:metal="http://xml.zope.org/namespaces/metal";
> +  xmlns:i18n="http://xml.zope.org/namespaces/i18n";
> +  tal:define="
> +      context_menu view/context/menu:context;
> +      features request/features"
> +  tal:condition="view/show_merge_links">
> +
> +  <h3>Branch merges</h3>
> +  <div id="merge-links"
> +       class="actions">
> +    <div id="merge-summary">
> +
> +      <div id="landing-candidates"
> +           tal:condition="view/landing_candidates">
> +        <img src="/@@/merge-proposal-icon" />
> +        <a href="+activereviews" tal:content="structure view/landing_candidate_count_text">
> +          1 branch
> +        </a>
> +        proposed for merging into this one.
> +
> +      </div>
> +
> +      <div id="dependent-landings" tal:condition="view/dependent_landings">
> +        <img src="/@@/merge-proposal-icon" />
> +        <a href="+merges" tal:content="structure view/dependent_landing_count_text">
> +          1 branch
> +        </a>
> +        dependent on this one.
> +      </div>
> +
> +      <div id="landing-targets" tal:condition="view/landing_targets">
> +        <tal:landing-candidates repeat="mergeproposal view/landing_targets">
> +          <tal:merge-fragment
> +              tal:replace="structure mergeproposal/@@+summary-fragment"/>
> +        </tal:landing-candidates>
> +      </div>
> +
> +    </div>
> +  </div>
> +
> +</div>
> 
> === modified file 'lib/lp/testing/factory.py'
> --- lib/lp/testing/factory.py	2015-04-22 16:11:40 +0000
> +++ lib/lp/testing/factory.py	2015-04-28 16:48:46 +0000
> @@ -1747,7 +1747,8 @@
>                  u"type": GitObjectType.COMMIT,
>                  }
>              for path in paths}
> -        return repository.createOrUpdateRefs(refs_info, get_objects=True)
> +        return removeSecurityProxy(repository).createOrUpdateRefs(
> +            refs_info, get_objects=True)
>  
>      def makeBug(self, target=None, owner=None, bug_watch_url=None,
>                  information_type=None, date_closed=None, title=None,
> 


-- 
https://code.launchpad.net/~cjwatson/launchpad/git-mp-basic-browser/+merge/257674
Your team Launchpad code reviewers is subscribed to branch lp:launchpad.


References