Merge lp:~cjwatson/launchpad/git-mp-commits into lp:launchpad

Proposed by Colin Watson on 2016-05-13
Status: Merged
Merged at revision: 18046
Proposed branch: lp:~cjwatson/launchpad/git-mp-commits
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/git-commits-link-mps
Diff against target: 524 lines (+213/-73)
11 files modified
lib/lp/code/browser/branchmergeproposal.py (+16/-4)
lib/lp/code/browser/tests/test_branchmergeproposal.py (+42/-7)
lib/lp/code/interfaces/branchmergeproposal.py (+4/-1)
lib/lp/code/model/branchmergeproposal.py (+54/-40)
lib/lp/code/stories/branches/xx-code-review-comments.txt (+50/-0)
lib/lp/code/templates/branchmergeproposal-index.pt (+8/-0)
lib/lp/code/templates/branchmergeproposal-resubmit.pt (+21/-11)
lib/lp/code/templates/codereviewcomment-reply.pt (+2/-2)
lib/lp/code/templates/codereviewnewrevisions-footer.pt (+13/-5)
lib/lp/code/templates/codereviewnewrevisions-header.pt (+1/-1)
lib/lp/code/templates/git-macros.pt (+2/-2)
To merge this branch: bzr merge lp:~cjwatson/launchpad/git-mp-commits
Reviewer Review Type Date Requested Status
William Grant code 2016-05-13 Approve on 2016-05-18
Review via email: mp+294671@code.launchpad.net

Commit message

Show unmerged and conversation-relevant Git commits in merge proposal views.

Description of the change

Show unmerged and conversation-relevant Git commits in merge proposal views.

The only significant oddity here is that we're using Git's idea of commit dates rather than when the commit was pushed. This will make sense from an author's point of view, but it will potentially mean that a reviewer sees commits pop up before their review comment. It arguably makes about as much sense as the approach we take with Bazaar, but it's certainly different. However, we can't emulate the Bazaar approach unless we start doing a full branch scanner and creating Revision rows for Git, which we wanted to avoid unless we had no choice, so let's go with this approach.

To post a comment you must log in.
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/browser/branchmergeproposal.py'
2--- lib/lp/code/browser/branchmergeproposal.py 2016-05-07 00:40:18 +0000
3+++ lib/lp/code/browser/branchmergeproposal.py 2016-05-18 22:37:48 +0000
4@@ -534,9 +534,15 @@
5 particular time.
6 """
7
8- def __init__(self, revisions, date, branch, diff):
9+ def __init__(self, revisions, date, source, diff):
10 self.revisions = revisions
11- self.branch = branch
12+ self.source = source
13+ if IBranch.providedBy(source):
14+ self.branch = source
15+ self.git_ref = None
16+ else:
17+ self.branch = None
18+ self.git_ref = source
19 self.has_body = False
20 self.has_footer = True
21 # The date attribute is used to sort the comments in the conversation.
22@@ -613,7 +619,9 @@
23 if IBranch.providedBy(source):
24 source = DecoratedBranch(source)
25 comments = []
26- if getFeatureFlag('code.incremental_diffs.enabled'):
27+ if (getFeatureFlag('code.incremental_diffs.enabled') and
28+ merge_proposal.source_branch is not None):
29+ # XXX cjwatson 2016-05-09: Implement for Git.
30 ranges = [
31 (revisions[0].revision.getLefthandParent(),
32 revisions[-1].revision)
33@@ -622,8 +630,12 @@
34 else:
35 diffs = [None] * len(groups)
36 for revisions, diff in zip(groups, diffs):
37+ if merge_proposal.source_branch is not None:
38+ last_date_created = revisions[-1].revision.date_created
39+ else:
40+ last_date_created = revisions[-1]["author_date"]
41 newrevs = CodeReviewNewRevisions(
42- revisions, revisions[-1].revision.date_created, source, diff)
43+ revisions, last_date_created, source, diff)
44 comments.append(newrevs)
45 while merge_proposal is not None:
46 from_superseded = merge_proposal != self.context
47
48=== modified file 'lib/lp/code/browser/tests/test_branchmergeproposal.py'
49--- lib/lp/code/browser/tests/test_branchmergeproposal.py 2016-05-18 12:15:05 +0000
50+++ lib/lp/code/browser/tests/test_branchmergeproposal.py 2016-05-18 22:37:48 +0000
51@@ -11,6 +11,7 @@
52 timedelta,
53 )
54 from difflib import unified_diff
55+import hashlib
56 import re
57
58 from lazr.lifecycle.event import ObjectModifiedEvent
59@@ -1031,9 +1032,12 @@
60
61
62 class TestBranchMergeProposalRequestReviewViewGit(
63- TestBranchMergeProposalRequestReviewViewMixin, BrowserTestCase):
64+ TestBranchMergeProposalRequestReviewViewMixin, GitHostingClientMixin,
65+ BrowserTestCase):
66 """Test `BranchMergeProposalRequestReviewView` for Git."""
67
68+ layer = LaunchpadFunctionalLayer
69+
70 def makeBranchMergeProposal(self):
71 return self.factory.makeBranchMergeProposalForGit()
72
73@@ -1261,10 +1265,10 @@
74 self.assertEqual('flibble', bmp.superseded_by.description)
75
76
77-class TestResubmitBrowserGit(BrowserTestCase):
78+class TestResubmitBrowserGit(GitHostingClientMixin, BrowserTestCase):
79 """Browser tests for resubmitting branch merge proposals for Git."""
80
81- layer = DatabaseFunctionalLayer
82+ layer = LaunchpadFunctionalLayer
83
84 def test_resubmit_text(self):
85 """The text of the resubmit page is as expected."""
86@@ -1441,7 +1445,7 @@
87 [diff],
88 [comment.diff for comment in comments])
89
90- def test_CodeReviewNewRevisions_implements_ICodeReviewNewRevisions(self):
91+ def test_CodeReviewNewRevisions_implements_interface_bzr(self):
92 # The browser helper class implements its interface.
93 review_date = datetime(2009, 9, 10, tzinfo=pytz.UTC)
94 revision_date = review_date + timedelta(days=1)
95@@ -1454,6 +1458,36 @@
96
97 self.assertTrue(verifyObject(ICodeReviewNewRevisions, new_revisions))
98
99+ def test_CodeReviewNewRevisions_implements_interface_git(self):
100+ # The browser helper class implements its interface.
101+ review_date = datetime(2009, 9, 10, tzinfo=pytz.UTC)
102+ author = self.factory.makePerson()
103+ with person_logged_in(author):
104+ author_email = author.preferredemail.email
105+ epoch = datetime.fromtimestamp(0, tz=pytz.UTC)
106+ review_date = self.factory.getUniqueDate()
107+ commit_date = self.factory.getUniqueDate()
108+ bmp = self.factory.makeBranchMergeProposalForGit(
109+ date_created=review_date)
110+ hosting_client = FakeMethod()
111+ hosting_client.getLog = FakeMethod(result=[
112+ {
113+ u'sha1': unicode(hashlib.sha1(b'0').hexdigest()),
114+ u'message': u'0',
115+ u'author': {
116+ u'name': author.display_name,
117+ u'email': author_email,
118+ u'time': int((commit_date - epoch).total_seconds()),
119+ },
120+ }
121+ ])
122+ self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
123+
124+ view = create_initialized_view(bmp, '+index')
125+ new_commits = view.conversation.comments[0]
126+
127+ self.assertTrue(verifyObject(ICodeReviewNewRevisions, new_commits))
128+
129 def test_include_superseded_comments(self):
130 for x, time in zip(range(3), time_counter()):
131 if x != 0:
132@@ -1527,9 +1561,10 @@
133 self.assertThat(browser.contents, HTMLContains(expected_meta))
134
135
136-class TestBranchMergeProposalBrowserView(BrowserTestCase):
137+class TestBranchMergeProposalBrowserView(
138+ GitHostingClientMixin, BrowserTestCase):
139
140- layer = DatabaseFunctionalLayer
141+ layer = LaunchpadFunctionalLayer
142
143 def test_prerequisite_bzr(self):
144 # A prerequisite branch is rendered in the Bazaar case.
145@@ -1727,7 +1762,7 @@
146 self.assertEqual('Eric on 2008-09-10', view.status_title)
147
148
149-class TestBranchMergeProposal(BrowserTestCase):
150+class TestBranchMergeProposal(GitHostingClientMixin, BrowserTestCase):
151
152 layer = LaunchpadFunctionalLayer
153
154
155=== modified file 'lib/lp/code/interfaces/branchmergeproposal.py'
156--- lib/lp/code/interfaces/branchmergeproposal.py 2015-10-19 10:56:16 +0000
157+++ lib/lp/code/interfaces/branchmergeproposal.py 2016-05-18 22:37:48 +0000
158@@ -1,4 +1,4 @@
159-# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
160+# Copyright 2009-2016 Canonical Ltd. This software is licensed under the
161 # GNU Affero General Public License version 3 (see the file LICENSE).
162
163 """The interface for branch merge proposals."""
164@@ -429,6 +429,9 @@
165 source branch that are not in the revision history of the target
166 branch. These are the revisions that have been committed to the
167 source branch since it branched off the target branch.
168+
169+ For Bazaar, this returns a sequence of `BranchRevision` objects.
170+ For Git, this returns a sequence of commit information dicts.
171 """
172
173 def getUsersVoteReference(user):
174
175=== modified file 'lib/lp/code/model/branchmergeproposal.py'
176--- lib/lp/code/model/branchmergeproposal.py 2015-10-13 17:24:59 +0000
177+++ lib/lp/code/model/branchmergeproposal.py 2016-05-18 22:37:48 +0000
178@@ -1,7 +1,7 @@
179-# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
180+# Copyright 2009-2016 Canonical Ltd. This software is licensed under the
181 # GNU Affero General Public License version 3 (see the file LICENSE).
182
183-"""Database class for branch merge prosals."""
184+"""Database class for branch merge proposals."""
185
186 __metaclass__ = type
187 __all__ = [
188@@ -36,10 +36,7 @@
189 Int,
190 Reference,
191 )
192-from storm.store import (
193- EmptyResultSet,
194- Store,
195- )
196+from storm.store import Store
197 from zope.component import getUtility
198 from zope.event import notify
199 from zope.interface import implementer
200@@ -70,8 +67,8 @@
201 IBranchMergeProposal,
202 IBranchMergeProposalGetter,
203 )
204-from lp.code.interfaces.branchrevision import IBranchRevision
205 from lp.code.interfaces.branchtarget import IHasBranchTarget
206+from lp.code.interfaces.codereviewcomment import ICodeReviewComment
207 from lp.code.interfaces.codereviewinlinecomment import (
208 ICodeReviewInlineCommentSet,
209 )
210@@ -785,26 +782,38 @@
211
212 def getUnlandedSourceBranchRevisions(self):
213 """See `IBranchMergeProposal`."""
214- if self.source_branch is None:
215- # XXX cjwatson 2015-04-16: Implement for Git somehow, perhaps by
216- # calling turnip via memcached.
217- return []
218- store = Store.of(self)
219- source = SQL("""source AS (SELECT BranchRevision.branch,
220- BranchRevision.revision, Branchrevision.sequence FROM
221- BranchRevision WHERE BranchRevision.branch = %s and
222- BranchRevision.sequence IS NOT NULL ORDER BY BranchRevision.branch
223- DESC, BranchRevision.sequence DESC
224- LIMIT 10)""" % self.source_branch.id)
225- where = SQL("""BranchRevision.revision NOT IN (SELECT revision from
226- BranchRevision AS target where target.branch = %s and
227- BranchRevision.revision = target.revision)""" %
228- self.target_branch.id)
229- using = SQL("""source as BranchRevision""")
230- revisions = store.with_(source).using(using).find(
231- BranchRevision, where)
232- return list(revisions.order_by(
233- Desc(BranchRevision.sequence)).config(limit=10))
234+ if self.source_branch is not None:
235+ store = Store.of(self)
236+ source = SQL("""
237+ source AS (
238+ SELECT
239+ BranchRevision.branch, BranchRevision.revision,
240+ Branchrevision.sequence
241+ FROM BranchRevision
242+ WHERE
243+ BranchRevision.branch = %s
244+ AND BranchRevision.sequence IS NOT NULL
245+ ORDER BY
246+ BranchRevision.branch DESC,
247+ BranchRevision.sequence DESC
248+ LIMIT 10)""" % self.source_branch.id)
249+ where = SQL("""
250+ BranchRevision.revision NOT IN (
251+ SELECT revision
252+ FROM BranchRevision AS target
253+ WHERE
254+ target.branch = %s
255+ AND BranchRevision.revision = target.revision)""" %
256+ self.target_branch.id)
257+ using = SQL("""source AS BranchRevision""")
258+ revisions = store.with_(source).using(using).find(
259+ BranchRevision, where)
260+ return list(revisions.order_by(
261+ Desc(BranchRevision.sequence)).config(limit=10))
262+ else:
263+ return self.source_git_ref.getCommits(
264+ self.source_git_commit_sha1, limit=10,
265+ stop=self.target_git_commit_sha1)
266
267 def createComment(self, owner, subject, content=None, vote=None,
268 review_type=None, parent=None, _date_created=DEFAULT,
269@@ -1031,34 +1040,39 @@
270 return None
271
272 def _getNewerRevisions(self):
273- if self.source_branch is None:
274- # XXX cjwatson 2015-04-16: Implement for Git.
275- return EmptyResultSet()
276 start_date = self.date_review_requested
277 if start_date is None:
278 start_date = self.date_created
279- return self.source_branch.getMainlineBranchRevisions(
280- start_date, self.revision_end_date, oldest_first=True)
281+ if self.source_branch is not None:
282+ revisions = self.source_branch.getMainlineBranchRevisions(
283+ start_date, self.revision_end_date, oldest_first=True)
284+ return [
285+ ((revision.date_created, branch_revision.sequence),
286+ branch_revision)
287+ for branch_revision, revision in revisions]
288+ else:
289+ commits = reversed(self.source_git_ref.getCommits(
290+ self.source_git_commit_sha1, stop=self.target_git_commit_sha1,
291+ start_date=start_date, end_date=self.revision_end_date))
292+ return [
293+ ((commit["author_date"], count), commit)
294+ for count, commit in enumerate(commits)]
295
296 def getRevisionsSinceReviewStart(self):
297 """Get the grouped revisions since the review started."""
298 entries = [
299 ((comment.date_created, -1), comment) for comment
300 in self.all_comments]
301- revisions = self._getNewerRevisions()
302- entries.extend(
303- ((revision.date_created, branch_revision.sequence),
304- branch_revision)
305- for branch_revision, revision in revisions)
306+ entries.extend(self._getNewerRevisions())
307 entries.sort()
308 current_group = []
309 for sortkey, entry in entries:
310- if IBranchRevision.providedBy(entry):
311- current_group.append(entry)
312- else:
313+ if ICodeReviewComment.providedBy(entry):
314 if current_group != []:
315 yield current_group
316 current_group = []
317+ else:
318+ current_group.append(entry)
319 if current_group != []:
320 yield current_group
321
322
323=== modified file 'lib/lp/code/stories/branches/xx-code-review-comments.txt'
324--- lib/lp/code/stories/branches/xx-code-review-comments.txt 2015-10-06 06:48:01 +0000
325+++ lib/lp/code/stories/branches/xx-code-review-comments.txt 2016-05-18 22:37:48 +0000
326@@ -188,6 +188,56 @@
327 4. By ... on 2009-09-12
328 and it works!
329
330+The same thing works for Git. Note that the hosting client returns newest
331+log entries first.
332+
333+ >>> from lp.code.interfaces.githosting import IGitHostingClient
334+ >>> from lp.testing.fakemethod import FakeMethod
335+ >>> from lp.testing.fixture import ZopeUtilityFixture
336+
337+ >>> login('admin@canonical.com')
338+ >>> bmp = factory.makeBranchMergeProposalForGit()
339+ >>> bmp.requestReview(review_date)
340+ >>> epoch = datetime.fromtimestamp(0, tz=pytz.UTC)
341+ >>> commit_date = review_date + timedelta(days=1)
342+ >>> hosting_client = FakeMethod()
343+ >>> hosting_client.getLog = FakeMethod(result=[])
344+ >>> for i in range(2):
345+ ... hosting_client.getLog.result.insert(0, {
346+ ... u'sha1': unicode(i * 2) * 40,
347+ ... u'message': u'Testing commits in conversation',
348+ ... u'author': {
349+ ... u'name': bmp.registrant.display_name,
350+ ... u'email': bmp.registrant.preferredemail.email,
351+ ... u'time': int((commit_date - epoch).total_seconds()),
352+ ... },
353+ ... })
354+ ... hosting_client.getLog.result.insert(0, {
355+ ... u'sha1': unicode(i * 2 + 1) * 40,
356+ ... u'message': u'and it works!',
357+ ... u'author': {
358+ ... u'name': bmp.registrant.display_name,
359+ ... u'email': bmp.registrant.preferredemail.email,
360+ ... u'time': int((commit_date - epoch).total_seconds()),
361+ ... },
362+ ... })
363+ ... commit_date += timedelta(days=1)
364+ >>> url = canonical_url(bmp)
365+ >>> logout()
366+
367+ >>> with ZopeUtilityFixture(hosting_client, IGitHostingClient):
368+ ... browser.open(url)
369+ >>> print_tag_with_id(browser.contents, 'conversation')
370+ ~.../+git/...:... updated on 2009-09-12 ...
371+ 0000000... by ... on 2009-09-11
372+ Testing commits in conversation
373+ 1111111... by ... on 2009-09-11
374+ and it works!
375+ 2222222... by ... on 2009-09-12
376+ Testing commits in conversation
377+ 3333333... by ... on 2009-09-12
378+ and it works!
379+
380
381 Inline Comments
382 ---------------
383
384=== modified file 'lib/lp/code/templates/branchmergeproposal-index.pt'
385--- lib/lp/code/templates/branchmergeproposal-index.pt 2015-04-22 16:11:40 +0000
386+++ lib/lp/code/templates/branchmergeproposal-index.pt 2016-05-18 22:37:48 +0000
387@@ -170,6 +170,14 @@
388 <p>Recent revisions are not available due to the source branch being remote.</p>
389 </tal:remote-branch>
390 </tal:bzr-revisions>
391+ <tal:git-revisions condition="context/source_git_ref">
392+ <tal:history-available condition="context/source_git_ref/has_commits"
393+ define="ref context/source_git_ref;
394+ commit_infos view/unlanded_revisions">
395+ <h2>Unmerged commits</h2>
396+ <metal:commits use-macro="ref/@@+macros/ref-commits"/>
397+ </tal:history-available>
398+ </tal:git-revisions>
399 </div>
400 </div>
401
402
403=== modified file 'lib/lp/code/templates/branchmergeproposal-resubmit.pt'
404--- lib/lp/code/templates/branchmergeproposal-resubmit.pt 2015-04-28 16:39:15 +0000
405+++ lib/lp/code/templates/branchmergeproposal-resubmit.pt 2016-05-18 22:37:48 +0000
406@@ -24,18 +24,28 @@
407 </div>
408 </div>
409
410- <div id="source-revisions" tal:condition="context/source_branch">
411- <tal:history-available condition="context/source_branch/revision_count"
412- define="branch context/source_branch;
413- revisions view/unlanded_revisions">
414- <h2>Unmerged revisions</h2>
415- <metal:landing-target use-macro="branch/@@+macros/branch-revisions"/>
416- </tal:history-available>
417+ <div id="source-revisions">
418+ <tal:bzr-revisions condition="context/source_branch">
419+ <tal:history-available condition="context/source_branch/revision_count"
420+ define="branch context/source_branch;
421+ revisions view/unlanded_revisions">
422+ <h2>Unmerged revisions</h2>
423+ <metal:landing-target use-macro="branch/@@+macros/branch-revisions"/>
424+ </tal:history-available>
425
426- <tal:remote-branch condition="context/source_branch/branch_type/enumvalue:REMOTE">
427- <h2>Unmerged revisions</h2>
428- <p>Recent revisions are not available due to the source branch being remote.</p>
429- </tal:remote-branch>
430+ <tal:remote-branch condition="context/source_branch/branch_type/enumvalue:REMOTE">
431+ <h2>Unmerged revisions</h2>
432+ <p>Recent revisions are not available due to the source branch being remote.</p>
433+ </tal:remote-branch>
434+ </tal:bzr-revisions>
435+ <tal:git-revisions condition="context/source_git_ref">
436+ <tal:history-available condition="context/source_git_ref/has_commits"
437+ define="ref context/source_git_ref;
438+ commit_infos view/unlanded_revisions">
439+ <h2>Unmerged commits</h2>
440+ <metal:commits use-macro="ref/@@+macros/ref-commits"/>
441+ </tal:history-available>
442+ </tal:git-revisions>
443 </div>
444
445 </div>
446
447=== modified file 'lib/lp/code/templates/codereviewcomment-reply.pt'
448--- lib/lp/code/templates/codereviewcomment-reply.pt 2016-01-21 03:23:01 +0000
449+++ lib/lp/code/templates/codereviewcomment-reply.pt 2016-05-18 22:37:48 +0000
450@@ -8,8 +8,8 @@
451
452 <body>
453 <h1 metal:fill-slot="heading"
454- tal:define="branch view/branch_merge_proposal/merge_source">
455- Code review comment for <tal:source content="branch/identity"/>
456+ tal:define="source view/branch_merge_proposal/merge_source">
457+ Code review comment for <tal:source content="source/identity"/>
458 </h1>
459
460 <div metal:fill-slot="main">
461
462=== modified file 'lib/lp/code/templates/codereviewnewrevisions-footer.pt'
463--- lib/lp/code/templates/codereviewnewrevisions-footer.pt 2015-06-23 17:30:39 +0000
464+++ lib/lp/code/templates/codereviewnewrevisions-footer.pt 2016-05-18 22:37:48 +0000
465@@ -3,11 +3,19 @@
466 xmlns:metal="http://xml.zope.org/namespaces/metal"
467 omit-tag="">
468
469- <tal:revisions define="branch context/branch;
470- revisions context/revisions;
471- show_diff_expander python:True;">
472- <metal:landing-target use-macro="branch/@@+macros/branch-revisions"/>
473- </tal:revisions>
474+ <tal:bzr-revisions condition="context/branch">
475+ <tal:revisions define="branch context/branch;
476+ revisions context/revisions;
477+ show_diff_expander python:True;">
478+ <metal:revisions use-macro="branch/@@+macros/branch-revisions"/>
479+ </tal:revisions>
480+ </tal:bzr-revisions>
481+ <tal:git-revisions condition="context/git_ref">
482+ <tal:revisions define="ref context/git_ref;
483+ commit_infos context/revisions;">
484+ <metal:commits use-macro="ref/@@+macros/ref-commits"/>
485+ </tal:revisions>
486+ </tal:git-revisions>
487 <tal:has-diff condition="context/diff">
488 <tal:diff condition="not: request/ss|nothing"
489 replace="structure context/diff/text/fmt:diff" />
490
491=== modified file 'lib/lp/code/templates/codereviewnewrevisions-header.pt'
492--- lib/lp/code/templates/codereviewnewrevisions-header.pt 2009-12-10 01:33:59 +0000
493+++ lib/lp/code/templates/codereviewnewrevisions-header.pt 2016-05-18 22:37:48 +0000
494@@ -3,7 +3,7 @@
495 xmlns:metal="http://xml.zope.org/namespaces/metal"
496 omit-tag="">
497
498- <tal:branch replace="structure context/branch/fmt:link"/>
499+ <tal:source replace="structure context/source/fmt:link"/>
500 updated
501 <tal:date replace="context/date/fmt:displaydate" />
502
503
504=== modified file 'lib/lp/code/templates/git-macros.pt'
505--- lib/lp/code/templates/git-macros.pt 2016-05-18 11:49:31 +0000
506+++ lib/lp/code/templates/git-macros.pt 2016-05-18 22:37:48 +0000
507@@ -135,7 +135,7 @@
508 sha1 python:commit_info['sha1'];
509 author python:commit_info['author'];
510 author_date python:commit_info['author_date']">
511- <a tal:attributes="href python: context.getCodebrowseUrlForRevision(sha1)"
512+ <a tal:attributes="href python: ref.getCodebrowseUrlForRevision(sha1)"
513 tal:content="sha1/fmt:shorten/10" />
514 by
515 <tal:known-person condition="author/person">
516@@ -153,7 +153,7 @@
517 replace="structure commit_message/fmt:obfuscate-email/fmt:text-to-html" />
518 </dd>
519
520- <div tal:define="merge_proposal python:commit_info['merge_proposal']"
521+ <div tal:define="merge_proposal python:commit_info.get('merge_proposal')"
522 tal:condition="merge_proposal">
523 <dd class="subordinate commit-comment"
524 tal:define="committer_date merge_proposal/merge_source/committer_date|nothing">