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

Proposed by Colin Watson
Status: Merged
Merged at revision: 17463
Proposed branch: lp:~cjwatson/launchpad/git-mp-basic-browser
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/storm-vocabulary-base
Diff against target: 1026 lines (+562/-85)
13 files modified
lib/lp/code/browser/branchmergeproposal.py (+95/-27)
lib/lp/code/browser/configure.zcml (+3/-0)
lib/lp/code/browser/gitref.py (+61/-0)
lib/lp/code/browser/tests/test_branchmergeproposal.py (+208/-54)
lib/lp/code/errors.py (+9/-2)
lib/lp/code/interfaces/gitrepository.py (+34/-0)
lib/lp/code/model/gitref.py (+3/-0)
lib/lp/code/model/gitrepository.py (+22/-0)
lib/lp/code/model/tests/test_gitrepository.py (+74/-0)
lib/lp/code/templates/branchmergeproposal-resubmit.pt (+1/-1)
lib/lp/code/templates/gitref-index.pt (+7/-0)
lib/lp/code/templates/gitref-pending-merges.pt (+43/-0)
lib/lp/testing/factory.py (+2/-1)
To merge this branch: bzr merge lp:~cjwatson/launchpad/git-mp-basic-browser
Reviewer Review Type Date Requested Status
William Grant (community) code Approve
Review via email: mp+257674@code.launchpad.net

This proposal supersedes a proposal from 2015-04-28.

Commit message

Add Git support to most of the merge proposal views.

Description of the change

Add Git support to most of the merge proposal views.

GitRef:+register-merge is about the same size again, and will be in a separate MP.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) wrote :

One bit of useless code, otherwise looks good.

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/browser/branchmergeproposal.py'
--- lib/lp/code/browser/branchmergeproposal.py 2015-04-22 16:41:41 +0000
+++ lib/lp/code/browser/branchmergeproposal.py 2015-04-29 12:39:54 +0000
@@ -86,6 +86,7 @@
86 InvalidBranchMergeProposal,86 InvalidBranchMergeProposal,
87 WrongBranchMergeProposal,87 WrongBranchMergeProposal,
88 )88 )
89from lp.code.interfaces.branch import IBranch
89from lp.code.interfaces.branchmergeproposal import IBranchMergeProposal90from lp.code.interfaces.branchmergeproposal import IBranchMergeProposal
90from lp.code.interfaces.codereviewcomment import ICodeReviewComment91from lp.code.interfaces.codereviewcomment import ICodeReviewComment
91from lp.code.interfaces.codereviewinlinecomment import (92from lp.code.interfaces.codereviewinlinecomment import (
@@ -355,7 +356,7 @@
355 @property356 @property
356 def codebrowse_url(self):357 def codebrowse_url(self):
357 """Return the link to codebrowse for this branch."""358 """Return the link to codebrowse for this branch."""
358 return self.context.source_branch.getCodebrowseUrl()359 return self.context.merge_source.getCodebrowseUrl()
359360
360361
361class BranchMergeProposalRevisionIdMixin:362class BranchMergeProposalRevisionIdMixin:
@@ -373,6 +374,9 @@
373 return None374 return None
374 # If the source branch is REMOTE, then there won't be any ids.375 # If the source branch is REMOTE, then there won't be any ids.
375 source_branch = self.context.source_branch376 source_branch = self.context.source_branch
377 if source_branch is None:
378 # Git doesn't have revision numbers. Just use the ids.
379 return revision_id
376 if source_branch.branch_type == BranchType.REMOTE:380 if source_branch.branch_type == BranchType.REMOTE:
377 return revision_id381 return revision_id
378 else:382 else:
@@ -586,12 +590,18 @@
586 def initialize(self):590 def initialize(self):
587 super(BranchMergeProposalView, self).initialize()591 super(BranchMergeProposalView, self).initialize()
588 cache = IJSONRequestCache(self.request)592 cache = IJSONRequestCache(self.request)
589 cache.objects.update({593 if IBranch.providedBy(self.context.merge_source):
590 'branch_diff_link':594 cache.objects.update({
591 'https://%s/+loggerhead/%s/diff/' % (595 'branch_diff_link':
592 config.launchpad.code_domain,596 'https://%s/+loggerhead/%s/diff/' % (
593 self.context.source_branch.unique_name),597 config.launchpad.code_domain,
594 })598 self.context.source_branch.unique_name),
599 })
600 else:
601 # XXX cjwatson 2015-04-29: Unimplemented for Git; this would
602 # require something in the webapp which proxies diff requests to
603 # the code browser, or an integrated code browser.
604 pass
595 if getFeatureFlag("longpoll.merge_proposals.enabled"):605 if getFeatureFlag("longpoll.merge_proposals.enabled"):
596 cache.objects['merge_proposal_event_key'] = subscribe(606 cache.objects['merge_proposal_event_key'] = subscribe(
597 self.context).event_key607 self.context).event_key
@@ -618,7 +628,9 @@
618 # Sort the comments by date order.628 # Sort the comments by date order.
619 merge_proposal = self.context629 merge_proposal = self.context
620 groups = list(merge_proposal.getRevisionsSinceReviewStart())630 groups = list(merge_proposal.getRevisionsSinceReviewStart())
621 source = DecoratedBranch(merge_proposal.source_branch)631 source = merge_proposal.merge_source
632 if IBranch.providedBy(source):
633 source = DecoratedBranch(source)
622 comments = []634 comments = []
623 if getFeatureFlag('code.incremental_diffs.enabled'):635 if getFeatureFlag('code.incremental_diffs.enabled'):
624 ranges = [636 ranges = [
@@ -685,6 +697,8 @@
685697
686 @property698 @property
687 def pending_diff(self):699 def pending_diff(self):
700 if self.context.source_branch is None:
701 return False
688 return (702 return (
689 self.context.next_preview_diff_job is not None or703 self.context.next_preview_diff_job is not None or
690 self.context.source_branch.pending_writes)704 self.context.source_branch.pending_writes)
@@ -705,6 +719,10 @@
705719
706 @property720 @property
707 def spec_links(self):721 def spec_links(self):
722 if self.context.source_branch is None:
723 # XXX cjwatson 2015-04-16: Implement once Git refs have linked
724 # blueprints.
725 return []
708 return list(726 return list(
709 self.context.source_branch.getSpecificationLinks(self.user))727 self.context.source_branch.getSpecificationLinks(self.user))
710728
@@ -748,12 +766,16 @@
748 @property766 @property
749 def status_config(self):767 def status_config(self):
750 """The config to configure the ChoiceSource JS widget."""768 """The config to configure the ChoiceSource JS widget."""
769 if IBranch.providedBy(self.context.merge_source):
770 source_revid = self.context.merge_source.last_scanned_id
771 else:
772 source_revid = self.context.merge_source.commit_sha1
751 return simplejson.dumps({773 return simplejson.dumps({
752 'status_widget_items': vocabulary_to_choice_edit_items(774 'status_widget_items': vocabulary_to_choice_edit_items(
753 self._createStatusVocabulary(),775 self._createStatusVocabulary(),
754 css_class_prefix='mergestatus'),776 css_class_prefix='mergestatus'),
755 'status_value': self.context.queue_status.title,777 'status_value': self.context.queue_status.title,
756 'source_revid': self.context.source_branch.last_scanned_id,778 'source_revid': source_revid,
757 'user_can_edit_status': check_permission(779 'user_can_edit_status': check_permission(
758 'launchpad.Edit', self.context),780 'launchpad.Edit', self.context),
759 })781 })
@@ -965,19 +987,35 @@
965 schema = ResubmitSchema987 schema = ResubmitSchema
966 for_input = True988 for_input = True
967 page_title = label = "Resubmit proposal to merge"989 page_title = label = "Resubmit proposal to merge"
968 field_names = [
969 'source_branch',
970 'target_branch',
971 'prerequisite_branch',
972 'description',
973 'break_link',
974 ]
975990
976 def initialize(self):991 def initialize(self):
977 self.cancel_url = canonical_url(self.context)992 self.cancel_url = canonical_url(self.context)
978 super(BranchMergeProposalResubmitView, self).initialize()993 super(BranchMergeProposalResubmitView, self).initialize()
979994
980 @property995 @property
996 def field_names(self):
997 if IBranch.providedBy(self.context.merge_source):
998 field_names = [
999 'source_branch',
1000 'target_branch',
1001 'prerequisite_branch',
1002 ]
1003 else:
1004 field_names = [
1005 'source_git_repository',
1006 'source_git_path',
1007 'target_git_repository',
1008 'target_git_path',
1009 'prerequisite_git_repository',
1010 'prerequisite_git_path',
1011 ]
1012 field_names.extend([
1013 'description',
1014 'break_link',
1015 ])
1016 return field_names
1017
1018 @property
981 def initial_values(self):1019 def initial_values(self):
982 UNSET = object()1020 UNSET = object()
983 items = ((key, getattr(self.context, key, UNSET)) for key in1021 items = ((key, getattr(self.context, key, UNSET)) for key in
@@ -988,10 +1026,24 @@
988 def resubmit_action(self, action, data):1026 def resubmit_action(self, action, data):
989 """Resubmit this proposal."""1027 """Resubmit this proposal."""
990 try:1028 try:
1029 if IBranch.providedBy(self.context.merge_source):
1030 merge_source = data['source_branch']
1031 merge_target = data['target_branch']
1032 merge_prerequisite = data['prerequisite_branch']
1033 else:
1034 merge_source = data['source_git_repository'].getRefByPath(
1035 data['source_git_path'])
1036 merge_target = data['target_git_repository'].getRefByPath(
1037 data['target_git_path'])
1038 if data['prerequisite_git_repository']:
1039 merge_prerequisite = (
1040 data['prerequisite_git_repository'].getRefByPath(
1041 data['prerequisite_git_path']))
1042 else:
1043 merge_prerequisite = None
991 proposal = self.context.resubmit(1044 proposal = self.context.resubmit(
992 self.user, data['source_branch'], data['target_branch'],1045 self.user, merge_source, merge_target, merge_prerequisite,
993 data['prerequisite_branch'], data['description'],1046 data['description'], data['break_link'])
994 data['break_link'])
995 except BranchMergeProposalExists as e:1047 except BranchMergeProposalExists as e:
996 message = structured(1048 message = structured(
997 'Cannot resubmit because <a href="%(url)s">a similar merge'1049 'Cannot resubmit because <a href="%(url)s">a similar merge'
@@ -1072,18 +1124,31 @@
1072 """The view to mark a merge proposal as merged."""1124 """The view to mark a merge proposal as merged."""
1073 schema = IBranchMergeProposal1125 schema = IBranchMergeProposal
1074 page_title = label = "Edit branch merge proposal"1126 page_title = label = "Edit branch merge proposal"
1075 field_names = ["merged_revno"]
1076 for_input = True1127 for_input = True
10771128
1078 @property1129 @property
1130 def field_names(self):
1131 if IBranch.providedBy(self.context.merge_target):
1132 return ["merged_revno"]
1133 else:
1134 return ["merged_revision_id"]
1135
1136 @property
1079 def initial_values(self):1137 def initial_values(self):
1080 # Default to the tip of the target branch, on the assumption that the1138 # Default to the tip of the target branch, on the assumption that the
1081 # source branch has just been merged into it.1139 # source branch has just been merged into it.
1082 if self.context.merged_revno is not None:1140 if IBranch.providedBy(self.context.merge_target):
1083 revno = self.context.merged_revno1141 if self.context.merged_revno is not None:
1142 revno = self.context.merged_revno
1143 else:
1144 revno = self.context.merge_target.revision_count
1145 return {'merged_revno': revno}
1084 else:1146 else:
1085 revno = self.context.target_branch.revision_count1147 if self.context.merged_revision_id is not None:
1086 return {'merged_revno': revno}1148 revision_id = self.context.merged_revision_id
1149 else:
1150 revision_id = self.context.merge_target.commit_sha1
1151 return {'merged_revision_id': revision_id}
10871152
1088 @property1153 @property
1089 def next_url(self):1154 def next_url(self):
@@ -1095,13 +1160,16 @@
1095 @notify1160 @notify
1096 def mark_merged_action(self, action, data):1161 def mark_merged_action(self, action, data):
1097 """Update the whiteboard and go back to the source branch."""1162 """Update the whiteboard and go back to the source branch."""
1098 revno = data['merged_revno']1163 if IBranch.providedBy(self.context.merge_target):
1164 kwargs = {'merged_revno': data['merged_revno']}
1165 else:
1166 kwargs = {'merged_revision_id': data['merged_revision_id']}
1099 if self.context.queue_status == BranchMergeProposalStatus.MERGED:1167 if self.context.queue_status == BranchMergeProposalStatus.MERGED:
1100 self.context.markAsMerged(merged_revno=revno)1168 self.context.markAsMerged(**kwargs)
1101 self.request.response.addNotification(1169 self.request.response.addNotification(
1102 'The proposal\'s merged revision has been updated.')1170 'The proposal\'s merged revision has been updated.')
1103 else:1171 else:
1104 self.context.markAsMerged(revno, merge_reporter=self.user)1172 self.context.markAsMerged(merge_reporter=self.user, **kwargs)
1105 self.request.response.addNotification(1173 self.request.response.addNotification(
1106 'The proposal has now been marked as merged.')1174 'The proposal has now been marked as merged.')
11071175
11081176
=== 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-29 12:39:54 +0000
@@ -824,6 +824,9 @@
824 <browser:page824 <browser:page
825 name="++ref-commits"825 name="++ref-commits"
826 template="../templates/gitref-commits.pt"/>826 template="../templates/gitref-commits.pt"/>
827 <browser:page
828 name="++ref-pending-merges"
829 template="../templates/gitref-pending-merges.pt"/>
827 </browser:pages>830 </browser:pages>
828 <browser:page831 <browser:page
829 for="lp.code.interfaces.gitref.IGitRef"832 for="lp.code.interfaces.gitref.IGitRef"
830833
=== 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-29 12:39:54 +0000
@@ -10,12 +10,19 @@
10 'GitRefView',10 'GitRefView',
11 ]11 ]
1212
13from lp.code.browser.branchmergeproposal import (
14 latest_proposals_for_each_branch,
15 )
13from lp.code.interfaces.gitref import IGitRef16from lp.code.interfaces.gitref import IGitRef
17from lp.code.model.gitrepository import GitRepository
18from lp.services.database.bulk import load_related
19from lp.services.propertycache import cachedproperty
14from lp.services.webapp import (20from lp.services.webapp import (
15 LaunchpadView,21 LaunchpadView,
16 Navigation,22 Navigation,
17 stepthrough,23 stepthrough,
18 )24 )
25from lp.services.webapp.authorization import check_permission
1926
2027
21class GitRefNavigation(Navigation):28class GitRefNavigation(Navigation):
@@ -49,3 +56,57 @@
49 "author_date": self.context.author_date,56 "author_date": self.context.author_date,
50 "commit_message": self.context.commit_message,57 "commit_message": self.context.commit_message,
51 }58 }
59
60 @property
61 def show_merge_links(self):
62 """Return whether or not merge proposal links should be shown.
63
64 Merge proposal links should not be shown if there is only one
65 reference in the entire target.
66 """
67 if not self.context.namespace.supports_merge_proposals:
68 return False
69 repositories = self.context.namespace.collection.getRepositories()
70 if repositories.count() > 1:
71 return True
72 repository = repositories.one()
73 if repository is None:
74 return False
75 return repository.refs.count() > 1
76
77 @cachedproperty
78 def landing_targets(self):
79 """Return a filtered list of landing targets."""
80 return latest_proposals_for_each_branch(self.context.landing_targets)
81
82 @cachedproperty
83 def landing_candidates(self):
84 """Return a decorated list of landing candidates."""
85 candidates = list(self.context.landing_candidates)
86 load_related(
87 GitRepository, candidates,
88 ["source_git_repositoryID", "prerequisite_git_repositoryID"])
89 return [proposal for proposal in candidates
90 if check_permission("launchpad.View", proposal)]
91
92 def _getBranchCountText(self, count):
93 """Help to show user friendly text."""
94 if count == 0:
95 return 'No branches'
96 elif count == 1:
97 return '1 branch'
98 else:
99 return '%s branches' % count
100
101 @cachedproperty
102 def landing_candidate_count_text(self):
103 return self._getBranchCountText(len(self.landing_candidates))
104
105 @cachedproperty
106 def dependent_landings(self):
107 return [proposal for proposal in self.context.dependent_landings
108 if check_permission("launchpad.View", proposal)]
109
110 @cachedproperty
111 def dependent_landing_count_text(self):
112 return self._getBranchCountText(len(self.dependent_landings))
52113
=== 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-29 12:39:54 +0000
@@ -46,6 +46,7 @@
46 BranchMergeProposalStatus,46 BranchMergeProposalStatus,
47 CodeReviewVote,47 CodeReviewVote,
48 )48 )
49from lp.code.interfaces.gitrepository import GIT_FEATURE_FLAG
49from lp.code.model.diff import PreviewDiff50from lp.code.model.diff import PreviewDiff
50from lp.code.tests.helpers import (51from lp.code.tests.helpers import (
51 add_revision_to_branch,52 add_revision_to_branch,
@@ -55,6 +56,7 @@
55 PersonVisibility,56 PersonVisibility,
56 TeamMembershipPolicy,57 TeamMembershipPolicy,
57 )58 )
59from lp.services.features.testing import FeatureFixture
58from lp.services.librarian.interfaces.client import LibrarianServerError60from lp.services.librarian.interfaces.client import LibrarianServerError
59from lp.services.messages.model.message import MessageSet61from lp.services.messages.model.message import MessageSet
60from lp.services.webapp import canonical_url62from lp.services.webapp import canonical_url
@@ -107,8 +109,8 @@
107 self.assertTrue(d.can_change_review)109 self.assertTrue(d.can_change_review)
108110
109111
110class TestBranchMergeProposalMergedView(TestCaseWithFactory):112class TestBranchMergeProposalMergedViewBzr(TestCaseWithFactory):
111 """Tests for `BranchMergeProposalMergedView`."""113 """Tests for `BranchMergeProposalMergedView` for Bazaar."""
112114
113 layer = DatabaseFunctionalLayer115 layer = DatabaseFunctionalLayer
114116
@@ -129,6 +131,30 @@
129 view.initial_values)131 view.initial_values)
130132
131133
134class TestBranchMergeProposalMergedViewGit(TestCaseWithFactory):
135 """Tests for `BranchMergeProposalMergedView` for Git."""
136
137 layer = DatabaseFunctionalLayer
138
139 def setUp(self):
140 # Use an admin so we don't have to worry about launchpad.Edit
141 # permissions on the merge proposals for adding comments, or
142 # nominating reviewers.
143 TestCaseWithFactory.setUp(self, user="admin@canonical.com")
144 self.useFixture(FeatureFixture({GIT_FEATURE_FLAG: u"on"}))
145 self.bmp = self.factory.makeBranchMergeProposalForGit()
146
147 def test_initial_values(self):
148 # The default merged_revision_id is the head commit SHA-1 of the
149 # target ref.
150 view = BranchMergeProposalMergedView(self.bmp, LaunchpadTestRequest())
151 removeSecurityProxy(self.bmp.source_git_ref).commit_sha1 = "0" * 40
152 removeSecurityProxy(self.bmp.target_git_ref).commit_sha1 = "1" * 40
153 self.assertEqual(
154 {'merged_revision_id': self.bmp.target_git_ref.commit_sha1},
155 view.initial_values)
156
157
132class TestBranchMergeProposalAddVoteView(TestCaseWithFactory):158class TestBranchMergeProposalAddVoteView(TestCaseWithFactory):
133 """Test the AddVote view."""159 """Test the AddVote view."""
134160
@@ -661,14 +687,14 @@
661 self.assertEqual(BrowserNotificationLevel.INFO, notification.level)687 self.assertEqual(BrowserNotificationLevel.INFO, notification.level)
662688
663689
664class TestBranchMergeProposalResubmitView(TestCaseWithFactory):690class TestBranchMergeProposalResubmitViewMixin:
665 """Test BranchMergeProposalResubmitView."""691 """Test BranchMergeProposalResubmitView."""
666692
667 layer = DatabaseFunctionalLayer693 layer = DatabaseFunctionalLayer
668694
669 def createView(self):695 def createView(self):
670 """Create the required view."""696 """Create the required view."""
671 context = self.factory.makeBranchMergeProposal()697 context = self._makeBranchMergeProposal()
672 self.useContext(person_logged_in(context.registrant))698 self.useContext(person_logged_in(context.registrant))
673 view = BranchMergeProposalResubmitView(699 view = BranchMergeProposalResubmitView(
674 context, LaunchpadTestRequest())700 context, LaunchpadTestRequest())
@@ -679,36 +705,34 @@
679 """resubmit_action resubmits the proposal."""705 """resubmit_action resubmits the proposal."""
680 view = self.createView()706 view = self.createView()
681 context = view.context707 context = view.context
682 new_proposal = view.resubmit_action.success(708 new_proposal = view.resubmit_action.success(self._getFormValues(
683 {'source_branch': context.source_branch,709 context.merge_source, context.merge_target,
684 'target_branch': context.target_branch,710 context.merge_prerequisite, {
685 'prerequisite_branch': context.prerequisite_branch,711 'description': None,
686 'description': None,712 'break_link': False,
687 'break_link': False,713 }))
688 })
689 self.assertEqual(new_proposal.supersedes, context)714 self.assertEqual(new_proposal.supersedes, context)
690 self.assertEqual(new_proposal.source_branch, context.source_branch)715 self.assertEqual(new_proposal.merge_source, context.merge_source)
691 self.assertEqual(new_proposal.target_branch, context.target_branch)716 self.assertEqual(new_proposal.merge_target, context.merge_target)
692 self.assertEqual(717 self.assertEqual(
693 new_proposal.prerequisite_branch, context.prerequisite_branch)718 new_proposal.merge_prerequisite, context.merge_prerequisite)
694719
695 def test_resubmit_action_change_branches(self):720 def test_resubmit_action_change_branches(self):
696 """Changing the branches changes the branches in the new proposal."""721 """Changing the branches changes the branches in the new proposal."""
697 view = self.createView()722 view = self.createView()
698 target = view.context.source_branch.target723 new_source = self._makeBranchWithSameTarget(view.context.merge_source)
699 new_source = self.factory.makeBranchTargetBranch(target)724 new_target = self._makeBranchWithSameTarget(view.context.merge_source)
700 new_target = self.factory.makeBranchTargetBranch(target)725 new_prerequisite = self._makeBranchWithSameTarget(
701 new_prerequisite = self.factory.makeBranchTargetBranch(target)726 view.context.merge_source)
702 new_proposal = view.resubmit_action.success(727 new_proposal = view.resubmit_action.success(self._getFormValues(
703 {'source_branch': new_source, 'target_branch': new_target,728 new_source, new_target, new_prerequisite, {
704 'prerequisite_branch': new_prerequisite,729 'description': 'description',
705 'description': 'description',730 'break_link': False,
706 'break_link': False,731 }))
707 })
708 self.assertEqual(new_proposal.supersedes, view.context)732 self.assertEqual(new_proposal.supersedes, view.context)
709 self.assertEqual(new_proposal.source_branch, new_source)733 self.assertEqual(new_proposal.merge_source, new_source)
710 self.assertEqual(new_proposal.target_branch, new_target)734 self.assertEqual(new_proposal.merge_target, new_target)
711 self.assertEqual(new_proposal.prerequisite_branch, new_prerequisite)735 self.assertEqual(new_proposal.merge_prerequisite, new_prerequisite)
712736
713 def test_resubmit_action_break_link(self):737 def test_resubmit_action_break_link(self):
714 """Enabling break_link prevents linking the old and new proposals."""738 """Enabling break_link prevents linking the old and new proposals."""
@@ -716,24 +740,22 @@
716 new_proposal = self.resubmitDefault(view, break_link=True)740 new_proposal = self.resubmitDefault(view, break_link=True)
717 self.assertIs(None, new_proposal.supersedes)741 self.assertIs(None, new_proposal.supersedes)
718742
719 @staticmethod743 @classmethod
720 def resubmitDefault(view, break_link=False, prerequisite_branch=None):744 def resubmitDefault(cls, view, break_link=False, merge_prerequisite=None):
721 context = view.context745 context = view.context
722 if prerequisite_branch is None:746 if merge_prerequisite is None:
723 prerequisite_branch = context.prerequisite_branch747 merge_prerequisite = context.merge_prerequisite
724 return view.resubmit_action.success(748 return view.resubmit_action.success(cls._getFormValues(
725 {'source_branch': context.source_branch,749 context.merge_source, context.merge_target, merge_prerequisite, {
726 'target_branch': context.target_branch,750 'description': None,
727 'prerequisite_branch': prerequisite_branch,751 'break_link': break_link,
728 'description': None,752 }))
729 'break_link': break_link,
730 })
731753
732 def test_resubmit_existing(self):754 def test_resubmit_existing(self):
733 """Resubmitting a proposal when another is active is a user error."""755 """Resubmitting a proposal when another is active is a user error."""
734 view = self.createView()756 view = self.createView()
735 first_bmp = view.context757 first_bmp = view.context
736 with person_logged_in(first_bmp.target_branch.owner):758 with person_logged_in(first_bmp.merge_target.owner):
737 first_bmp.resubmit(first_bmp.registrant)759 first_bmp.resubmit(first_bmp.registrant)
738 self.resubmitDefault(view)760 self.resubmitDefault(view)
739 (notification,) = view.request.response.notifications761 (notification,) = view.request.response.notifications
@@ -742,19 +764,86 @@
742 ' <a href=.*>a similar merge proposal</a> is already active.'))764 ' <a href=.*>a similar merge proposal</a> is already active.'))
743 self.assertEqual(BrowserNotificationLevel.ERROR, notification.level)765 self.assertEqual(BrowserNotificationLevel.ERROR, notification.level)
744766
767
768class TestBranchMergeProposalResubmitViewBzr(
769 TestBranchMergeProposalResubmitViewMixin, TestCaseWithFactory):
770 """Test BranchMergeProposalResubmitView for Bazaar."""
771
772 def _makeBranchMergeProposal(self):
773 return self.factory.makeBranchMergeProposal()
774
775 @staticmethod
776 def _getFormValues(source_branch, target_branch, prerequisite_branch,
777 extras):
778 values = {
779 'source_branch': source_branch,
780 'target_branch': target_branch,
781 'prerequisite_branch': prerequisite_branch,
782 }
783 values.update(extras)
784 return values
785
786 def _makeBranchWithSameTarget(self, branch):
787 return self.factory.makeBranchTargetBranch(branch.target)
788
745 def test_resubmit_same_target_prerequisite(self):789 def test_resubmit_same_target_prerequisite(self):
746 """User error if same branch is target and prerequisite."""790 """User error if same branch is target and prerequisite."""
747 view = self.createView()791 view = self.createView()
748 first_bmp = view.context792 first_bmp = view.context
749 self.resubmitDefault(793 self.resubmitDefault(view, merge_prerequisite=first_bmp.merge_target)
750 view, prerequisite_branch=first_bmp.target_branch)
751 self.assertEqual(794 self.assertEqual(
752 view.errors,795 view.errors,
753 ['Target and prerequisite branches must be different.'])796 ['Target and prerequisite branches must be different.'])
754797
755798
756class TestResubmitBrowser(BrowserTestCase):799class TestBranchMergeProposalResubmitViewGit(
757 """Browser tests for resubmitting branch merge proposals."""800 TestBranchMergeProposalResubmitViewMixin, TestCaseWithFactory):
801 """Test BranchMergeProposalResubmitView for Git."""
802
803 def setUp(self):
804 super(TestBranchMergeProposalResubmitViewGit, self).setUp()
805 self.useFixture(FeatureFixture({GIT_FEATURE_FLAG: u"on"}))
806
807 def _makeBranchMergeProposal(self):
808 return self.factory.makeBranchMergeProposalForGit()
809
810 @staticmethod
811 def _getFormValues(source_branch, target_branch, prerequisite_branch,
812 extras):
813 values = {
814 'source_git_repository': source_branch.repository,
815 'source_git_path': source_branch.path,
816 'target_git_repository': target_branch.repository,
817 'target_git_path': target_branch.path,
818 }
819 if prerequisite_branch is not None:
820 values.update({
821 'prerequisite_git_repository': prerequisite_branch.repository,
822 'prerequisite_git_path': prerequisite_branch.path,
823 })
824 else:
825 values.update({
826 'prerequisite_git_repository': None,
827 'prerequisite_git_path': '',
828 })
829 values.update(extras)
830 return values
831
832 def _makeBranchWithSameTarget(self, branch):
833 return self.factory.makeGitRefs(target=branch.target)[0]
834
835 def test_resubmit_same_target_prerequisite(self):
836 """User error if same branch is target and prerequisite."""
837 view = self.createView()
838 first_bmp = view.context
839 self.resubmitDefault(view, merge_prerequisite=first_bmp.merge_target)
840 self.assertEqual(
841 view.errors,
842 ['Target and prerequisite references must be different.'])
843
844
845class TestResubmitBrowserBzr(BrowserTestCase):
846 """Browser tests for resubmitting branch merge proposals for Bazaar."""
758847
759 layer = DatabaseFunctionalLayer848 layer = DatabaseFunctionalLayer
760849
@@ -781,6 +870,41 @@
781 self.assertEqual('flibble', bmp.superseded_by.description)870 self.assertEqual('flibble', bmp.superseded_by.description)
782871
783872
873class TestResubmitBrowserGit(BrowserTestCase):
874 """Browser tests for resubmitting branch merge proposals for Git."""
875
876 layer = DatabaseFunctionalLayer
877
878 def setUp(self):
879 super(TestResubmitBrowserGit, self).setUp()
880 self.useFixture(FeatureFixture({GIT_FEATURE_FLAG: u"on"}))
881
882 def test_resubmit_text(self):
883 """The text of the resubmit page is as expected."""
884 bmp = self.factory.makeBranchMergeProposalForGit(registrant=self.user)
885 text = self.getMainText(bmp, '+resubmit')
886 expected = (
887 'Resubmit proposal to merge.*'
888 'Source Git Repository:.*'
889 'Source Git branch path:.*'
890 'Target Git Repository:.*'
891 'Target Git branch path:.*'
892 'Prerequisite Git Repository:.*'
893 'Prerequisite Git branch path:.*'
894 'Description.*'
895 'Start afresh.*')
896 self.assertTextMatchesExpressionIgnoreWhitespace(expected, text)
897
898 def test_resubmit_controls(self):
899 """Proposals can be resubmitted using the browser."""
900 bmp = self.factory.makeBranchMergeProposalForGit(registrant=self.user)
901 browser = self.getViewBrowser(bmp, '+resubmit')
902 browser.getControl('Description').value = 'flibble'
903 browser.getControl('Resubmit').click()
904 with person_logged_in(self.user):
905 self.assertEqual('flibble', bmp.superseded_by.description)
906
907
784class TestBranchMergeProposalView(TestCaseWithFactory):908class TestBranchMergeProposalView(TestCaseWithFactory):
785909
786 layer = LaunchpadFunctionalLayer910 layer = LaunchpadFunctionalLayer
@@ -1248,17 +1372,17 @@
1248 self.assertEqual(mp_url, browser.url)1372 self.assertEqual(mp_url, browser.url)
12491373
12501374
1251class TestLatestProposalsForEachBranch(TestCaseWithFactory):1375class TestLatestProposalsForEachBranchMixin:
1252 """Confirm that the latest branch is returned."""1376 """Confirm that the latest branch is returned."""
12531377
1254 layer = DatabaseFunctionalLayer1378 layer = DatabaseFunctionalLayer
12551379
1256 def test_newest_first(self):1380 def test_newest_first(self):
1257 # If each proposal targets a different branch, each will be returned.1381 # If each proposal targets a different branch, each will be returned.
1258 bmp1 = self.factory.makeBranchMergeProposal(1382 bmp1 = self._makeBranchMergeProposal(
1259 date_created=(1383 date_created=(
1260 datetime(year=2008, month=9, day=10, tzinfo=pytz.UTC)))1384 datetime(year=2008, month=9, day=10, tzinfo=pytz.UTC)))
1261 bmp2 = self.factory.makeBranchMergeProposal(1385 bmp2 = self._makeBranchMergeProposal(
1262 date_created=(1386 date_created=(
1263 datetime(year=2008, month=10, day=10, tzinfo=pytz.UTC)))1387 datetime(year=2008, month=10, day=10, tzinfo=pytz.UTC)))
1264 self.assertEqual(1388 self.assertEqual(
@@ -1266,27 +1390,57 @@
12661390
1267 def test_visible_filtered_out(self):1391 def test_visible_filtered_out(self):
1268 # If the proposal is not visible to the user, they are not returned.1392 # If the proposal is not visible to the user, they are not returned.
1269 bmp1 = self.factory.makeBranchMergeProposal(1393 bmp1 = self._makeBranchMergeProposal(
1270 date_created=(1394 date_created=(
1271 datetime(year=2008, month=9, day=10, tzinfo=pytz.UTC)))1395 datetime(year=2008, month=9, day=10, tzinfo=pytz.UTC)))
1272 bmp2 = self.factory.makeBranchMergeProposal(1396 bmp2 = self._makeBranchMergeProposal(
1273 date_created=(1397 date_created=(
1274 datetime(year=2008, month=10, day=10, tzinfo=pytz.UTC)))1398 datetime(year=2008, month=10, day=10, tzinfo=pytz.UTC)))
1275 removeSecurityProxy(bmp2.source_branch).transitionToInformationType(1399 self._setBranchInvisible(bmp2.merge_source)
1276 InformationType.USERDATA, bmp2.source_branch.owner,
1277 verify_policy=False)
1278 self.assertEqual(1400 self.assertEqual(
1279 [bmp1], latest_proposals_for_each_branch([bmp1, bmp2]))1401 [bmp1], latest_proposals_for_each_branch([bmp1, bmp2]))
12801402
1281 def test_same_target(self):1403 def test_same_target(self):
1282 # If the proposals target the same branch, then the most recent is1404 # If the proposals target the same branch, then the most recent is
1283 # returned.1405 # returned.
1284 bmp1 = self.factory.makeBranchMergeProposal(1406 bmp1 = self._makeBranchMergeProposal(
1285 date_created=(1407 date_created=(
1286 datetime(year=2008, month=9, day=10, tzinfo=pytz.UTC)))1408 datetime(year=2008, month=9, day=10, tzinfo=pytz.UTC)))
1287 bmp2 = self.factory.makeBranchMergeProposal(1409 bmp2 = self._makeBranchMergeProposal(
1288 target_branch=bmp1.target_branch,1410 merge_target=bmp1.merge_target,
1289 date_created=(1411 date_created=(
1290 datetime(year=2008, month=10, day=10, tzinfo=pytz.UTC)))1412 datetime(year=2008, month=10, day=10, tzinfo=pytz.UTC)))
1291 self.assertEqual(1413 self.assertEqual(
1292 [bmp2], latest_proposals_for_each_branch([bmp1, bmp2]))1414 [bmp2], latest_proposals_for_each_branch([bmp1, bmp2]))
1415
1416
1417class TestLatestProposalsForEachBranchBzr(
1418 TestLatestProposalsForEachBranchMixin, TestCaseWithFactory):
1419 """Confirm that the latest branch is returned for Bazaar."""
1420
1421 def _makeBranchMergeProposal(self, merge_target=None, **kwargs):
1422 return self.factory.makeBranchMergeProposal(
1423 target_branch=merge_target, **kwargs)
1424
1425 @staticmethod
1426 def _setBranchInvisible(branch):
1427 removeSecurityProxy(branch).transitionToInformationType(
1428 InformationType.USERDATA, branch.owner, verify_policy=False)
1429
1430
1431class TestLatestProposalsForEachBranchGit(
1432 TestLatestProposalsForEachBranchMixin, TestCaseWithFactory):
1433 """Confirm that the latest branch is returned for Bazaar."""
1434
1435 def setUp(self):
1436 super(TestLatestProposalsForEachBranchGit, self).setUp()
1437 self.useFixture(FeatureFixture({GIT_FEATURE_FLAG: u"on"}))
1438
1439 def _makeBranchMergeProposal(self, merge_target=None, **kwargs):
1440 return self.factory.makeBranchMergeProposalForGit(
1441 target_ref=merge_target, **kwargs)
1442
1443 @staticmethod
1444 def _setBranchInvisible(branch):
1445 removeSecurityProxy(branch.repository).transitionToInformationType(
1446 InformationType.USERDATA, branch.owner, verify_policy=False)
12931447
=== 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-29 12:39:54 +0000
@@ -231,11 +231,18 @@
231 """Raised if there is already a matching BranchMergeProposal."""231 """Raised if there is already a matching BranchMergeProposal."""
232232
233 def __init__(self, existing_proposal):233 def __init__(self, existing_proposal):
234 # Circular import.
235 from lp.code.interfaces.branch import IBranch
236 # display_name is the newer style, but IBranch uses the older style.
237 if IBranch.providedBy(existing_proposal.merge_source):
238 display_name = "displayname"
239 else:
240 display_name = "display_name"
234 super(BranchMergeProposalExists, self).__init__(241 super(BranchMergeProposalExists, self).__init__(
235 'There is already a branch merge proposal registered for '242 'There is already a branch merge proposal registered for '
236 'branch %s to land on %s that is still active.' %243 'branch %s to land on %s that is still active.' %
237 (existing_proposal.source_branch.displayname,244 (getattr(existing_proposal.merge_source, display_name),
238 existing_proposal.target_branch.displayname))245 getattr(existing_proposal.merge_target, display_name)))
239 self.existing_proposal = existing_proposal246 self.existing_proposal = existing_proposal
240247
241248
242249
=== 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-29 12:39:54 +0000
@@ -49,6 +49,7 @@
49 Choice,49 Choice,
50 Datetime,50 Datetime,
51 Int,51 Int,
52 List,
52 Text,53 Text,
53 TextLine,54 TextLine,
54 )55 )
@@ -607,6 +608,39 @@
607 :return: A collection of `IGitRepository` objects.608 :return: A collection of `IGitRepository` objects.
608 """609 """
609610
611 @call_with(user=REQUEST_USER)
612 @operation_parameters(
613 person=Reference(
614 title=_("The person whose repository visibility is being "
615 "checked."),
616 schema=IPerson),
617 repository_names=List(value_type=Text(),
618 title=_('List of repository unique names'), required=True),
619 )
620 @export_read_operation()
621 @operation_for_version("devel")
622 def getRepositoryVisibilityInfo(user, person, repository_names):
623 """Return the named repositories visible to both user and person.
624
625 Anonymous requesters don't get any information.
626
627 :param user: The user requesting the information. If the user is
628 None then we return an empty dict.
629 :param person: The person whose repository visibility we wish to
630 check.
631 :param repository_names: The unique names of the repositories to
632 check.
633
634 Return a dict with the following values:
635 person_name: the displayname of the person.
636 visible_repositories: a list of the unique names of the repositories
637 which the requester and specified person can both see.
638
639 This API call is provided for use by the client Javascript. It is
640 not designed to efficiently scale to handle requests for large
641 numbers of repositories.
642 """
643
610 @operation_parameters(644 @operation_parameters(
611 target=Reference(645 target=Reference(
612 title=_("Target"), required=True, schema=IHasGitRepositories))646 title=_("Target"), required=True, schema=IHasGitRepositories))
613647
=== 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-29 12:39:54 +0000
@@ -376,3 +376,6 @@
376376
377 def __ne__(self, other):377 def __ne__(self, other):
378 return not self == other378 return not self == other
379
380 def __hash__(self):
381 return hash(self.repository) ^ hash(self.path) ^ hash(self.commit_sha1)
379382
=== 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-29 12:39:54 +0000
@@ -38,6 +38,7 @@
38from storm.store import Store38from storm.store import Store
39from zope.component import getUtility39from zope.component import getUtility
40from zope.interface import implements40from zope.interface import implements
41from zope.security.interfaces import Unauthorized
41from zope.security.proxy import removeSecurityProxy42from zope.security.proxy import removeSecurityProxy
4243
43from lp.app.enums import (44from lp.app.enums import (
@@ -774,6 +775,27 @@
774 collection = IGitCollection(target).visibleByUser(user)775 collection = IGitCollection(target).visibleByUser(user)
775 return collection.getRepositories(eager_load=True)776 return collection.getRepositories(eager_load=True)
776777
778 def getRepositoryVisibilityInfo(self, user, person, repository_names):
779 """See `IGitRepositorySet`."""
780 if user is None:
781 return dict()
782 lookup = getUtility(IGitLookup)
783 visible_repositories = []
784 for name in repository_names:
785 repository = lookup.getByUniqueName(name)
786 try:
787 if (repository is not None
788 and repository.visibleByUser(user)
789 and repository.visibleByUser(person)):
790 visible_repositories.append(repository.unique_name)
791 except Unauthorized:
792 # We don't include repositories user cannot see.
793 pass
794 return {
795 'person_name': person.displayname,
796 'visible_repositories': visible_repositories,
797 }
798
777 def getDefaultRepository(self, target):799 def getDefaultRepository(self, target):
778 """See `IGitRepositorySet`."""800 """See `IGitRepositorySet`."""
779 clauses = [GitRepository.target_default == True]801 clauses = [GitRepository.target_default == True]
780802
=== 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-29 12:39:54 +0000
@@ -1216,6 +1216,80 @@
1216 public_repositories + [private_repository],1216 public_repositories + [private_repository],
1217 self.repository_set.getRepositories(other_person, project))1217 self.repository_set.getRepositories(other_person, project))
12181218
1219 def test_getRepositoryVisibilityInfo_empty_repository_names(self):
1220 # If repository_names is empty, getRepositoryVisibilityInfo returns
1221 # an empty visible_repositories list.
1222 person = self.factory.makePerson(name="fred")
1223 info = self.repository_set.getRepositoryVisibilityInfo(
1224 person, person, repository_names=[])
1225 self.assertEqual("Fred", info["person_name"])
1226 self.assertEqual([], info["visible_repositories"])
1227
1228 def test_getRepositoryVisibilityInfo(self):
1229 person = self.factory.makePerson(name="fred")
1230 owner = self.factory.makePerson()
1231 visible_repository = self.factory.makeGitRepository()
1232 invisible_repository = self.factory.makeGitRepository(
1233 owner=owner, information_type=InformationType.USERDATA)
1234 invisible_name = removeSecurityProxy(invisible_repository).unique_name
1235 repositories = [visible_repository.unique_name, invisible_name]
1236
1237 with person_logged_in(owner):
1238 info = self.repository_set.getRepositoryVisibilityInfo(
1239 owner, person, repository_names=repositories)
1240 self.assertEqual("Fred", info["person_name"])
1241 self.assertEqual(
1242 [visible_repository.unique_name], info["visible_repositories"])
1243
1244 def test_getRepositoryVisibilityInfo_unauthorised_user(self):
1245 # If the user making the API request cannot see one of the
1246 # repositories, that repository is not included in the results.
1247 person = self.factory.makePerson(name="fred")
1248 owner = self.factory.makePerson()
1249 visible_repository = self.factory.makeGitRepository()
1250 invisible_repository = self.factory.makeGitRepository(
1251 owner=owner, information_type=InformationType.USERDATA)
1252 invisible_name = removeSecurityProxy(invisible_repository).unique_name
1253 repositories = [visible_repository.unique_name, invisible_name]
1254
1255 someone = self.factory.makePerson()
1256 with person_logged_in(someone):
1257 info = self.repository_set.getRepositoryVisibilityInfo(
1258 someone, person, repository_names=repositories)
1259 self.assertEqual("Fred", info["person_name"])
1260 self.assertEqual(
1261 [visible_repository.unique_name], info["visible_repositories"])
1262
1263 def test_getRepositoryVisibilityInfo_anonymous(self):
1264 # Anonymous users are not allowed to see any repository visibility
1265 # information, even if the repository they are querying about is
1266 # public.
1267 person = self.factory.makePerson(name="fred")
1268 owner = self.factory.makePerson()
1269 visible_repository = self.factory.makeGitRepository(owner=owner)
1270 repositories = [visible_repository.unique_name]
1271
1272 with person_logged_in(owner):
1273 info = self.repository_set.getRepositoryVisibilityInfo(
1274 None, person, repository_names=repositories)
1275 self.assertEqual({}, info)
1276
1277 def test_getRepositoryVisibilityInfo_invalid_repository_name(self):
1278 # If an invalid repository name is specified, it is not included.
1279 person = self.factory.makePerson(name="fred")
1280 owner = self.factory.makePerson()
1281 visible_repository = self.factory.makeGitRepository(owner=owner)
1282 repositories = [
1283 visible_repository.unique_name,
1284 "invalid_repository_name"]
1285
1286 with person_logged_in(owner):
1287 info = self.repository_set.getRepositoryVisibilityInfo(
1288 owner, person, repository_names=repositories)
1289 self.assertEqual("Fred", info["person_name"])
1290 self.assertEqual(
1291 [visible_repository.unique_name], info["visible_repositories"])
1292
1219 def test_setDefaultRepository_refuses_person(self):1293 def test_setDefaultRepository_refuses_person(self):
1220 # setDefaultRepository refuses if the target is a person.1294 # setDefaultRepository refuses if the target is a person.
1221 person = self.factory.makePerson()1295 person = self.factory.makePerson()
12221296
=== 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-29 12:39:54 +0000
@@ -24,7 +24,7 @@
24 </div>24 </div>
25 </div>25 </div>
2626
27 <div id="source-revisions">27 <div id="source-revisions" tal:condition="context/source_branch">
28 <tal:history-available condition="context/source_branch/revision_count"28 <tal:history-available condition="context/source_branch/revision_count"
29 define="branch context/source_branch;29 define="branch context/source_branch;
30 revisions view/unlanded_revisions">30 revisions view/unlanded_revisions">
3131
=== 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-29 12:39:54 +0000
@@ -17,6 +17,13 @@
17<div metal:fill-slot="main">17<div metal:fill-slot="main">
1818
19 <div class="yui-g">19 <div class="yui-g">
20 <div id="ref-relations" class="portlet">
21 <tal:ref-pending-merges
22 replace="structure context/@@++ref-pending-merges" />
23 </div>
24 </div>
25
26 <div class="yui-g">
20 <div id="ref-info" class="portlet">27 <div id="ref-info" class="portlet">
21 <h2>Branch information</h2>28 <h2>Branch information</h2>
22 <div class="two-column-list">29 <div class="two-column-list">
2330
=== 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-29 12:39:54 +0000
@@ -0,0 +1,43 @@
1<div
2 xmlns:tal="http://xml.zope.org/namespaces/tal"
3 xmlns:metal="http://xml.zope.org/namespaces/metal"
4 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
5 tal:define="
6 context_menu view/context/menu:context;
7 features request/features"
8 tal:condition="view/show_merge_links">
9
10 <h3>Branch merges</h3>
11 <div id="merge-links"
12 class="actions">
13 <div id="merge-summary">
14
15 <div id="landing-candidates"
16 tal:condition="view/landing_candidates">
17 <img src="/@@/merge-proposal-icon" />
18 <a href="+activereviews" tal:content="structure view/landing_candidate_count_text">
19 1 branch
20 </a>
21 proposed for merging into this one.
22
23 </div>
24
25 <div id="dependent-landings" tal:condition="view/dependent_landings">
26 <img src="/@@/merge-proposal-icon" />
27 <a href="+merges" tal:content="structure view/dependent_landing_count_text">
28 1 branch
29 </a>
30 dependent on this one.
31 </div>
32
33 <div id="landing-targets" tal:condition="view/landing_targets">
34 <tal:landing-candidates repeat="mergeproposal view/landing_targets">
35 <tal:merge-fragment
36 tal:replace="structure mergeproposal/@@+summary-fragment"/>
37 </tal:landing-candidates>
38 </div>
39
40 </div>
41 </div>
42
43</div>
044
=== 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-29 12:39:54 +0000
@@ -1747,7 +1747,8 @@
1747 u"type": GitObjectType.COMMIT,1747 u"type": GitObjectType.COMMIT,
1748 }1748 }
1749 for path in paths}1749 for path in paths}
1750 return repository.createOrUpdateRefs(refs_info, get_objects=True)1750 return removeSecurityProxy(repository).createOrUpdateRefs(
1751 refs_info, get_objects=True)
17511752
1752 def makeBug(self, target=None, owner=None, bug_watch_url=None,1753 def makeBug(self, target=None, owner=None, bug_watch_url=None,
1753 information_type=None, date_closed=None, title=None,1754 information_type=None, date_closed=None, title=None,