Repository: fedora-infra/bodhi

Issue:
Update IDs in comments should be automatically linkified
For example, when mentioning another update in a comment:

https://bodhi.fedoraproject.org/updates/eclipse-remote-2.0.1-1.fc23#comment-386091

It should be automatically turned into a link, in much the same way bugzilla does with bug numbers.



Discussion:
If that can help, pagure has a few customization in its markdow processor: https://pagure.io/pagure/blob/master/f/pagure/pfmarkdown.py


Resolving patch:
diff --git a/bodhi/server/ffmarkdown.py b/bodhi/server/ffmarkdown.py
--- a/bodhi/server/ffmarkdown.py
+++ b/bodhi/server/ffmarkdown.py
@@ -22,6 +22,7 @@
 Author: Ralph Bean <rbean@redhat.com>
 """
 
+from re import escape
 import typing
 
 from markdown.extensions import Extension
@@ -31,6 +32,7 @@
 import pyramid.threadlocal
 
 from bodhi import MENTION_RE
+from bodhi.server.config import config
 
 if typing.TYPE_CHECKING:  # pragma: no cover
     import re  # noqa: 401
@@ -38,6 +40,9 @@
 
 
 BUGZILLA_RE = r'([a-zA-Z]+)(#[0-9]{5,})'
+UPDATE_RE = (r'(?:(?<!\S)|('
+             + escape(config['base_address'])
+             + r'updates/))([A-Z\-]+-\d{4}-[^\W_]{10})(?:(?=[\.,;:])|(?!\S))')
 
 
 def user_url(name: str) -> str:
@@ -86,6 +91,19 @@ def bug_url(tracker: str, idx: typing.Union[int, str]) -> typing.Optional[str]:
         return None
 
 
+def update_url(alias: str) -> str:
+    """
+    Return a URL to the given update.
+
+    Args:
+        alias: The alias of the update.
+    Returns:
+        A URL to the requested update.
+    """
+    request = pyramid.threadlocal.get_current_request()
+    return request.route_url('update', id=alias)
+
+
 class MentionPattern(markdown.inlinepatterns.Pattern):
     """Match username mentions and point to their profiles."""
 
@@ -130,6 +148,27 @@ def handleMatch(self, m: 're.Match') -> 'xml.etree.ElementTree.Element':
         return el
 
 
+class UpdatePattern(markdown.inlinepatterns.Pattern):
+    """Match update alias pattern and link to the update."""
+
+    def handleMatch(self, m: 're.Match') -> 'xml.etree.ElementTree.Element':
+        """
+        Build and return an Element that links to the referenced update.
+
+        Args:
+            m: The regex match on the update.
+        Returns:
+            An html anchor referencing the matched update.
+        """
+        alias = markdown.util.AtomicString(m.group(3))
+        url = update_url(alias)
+
+        el = markdown.util.etree.Element("a")
+        el.set('href', url)
+        el.text = alias
+        return el
+
+
 class SurroundProcessor(markdown.postprocessors.Postprocessor):
     """A postprocessor to surround the text with a markdown <div>."""
 
@@ -158,4 +197,5 @@ def extendMarkdown(self, md: markdown.Markdown, md_globals: dict) -> None:
         """
         md.inlinePatterns.add('mention', MentionPattern(MENTION_RE, md), '_end')
         md.inlinePatterns.add('bugzilla', BugzillaPattern(BUGZILLA_RE, md), '_end')
+        md.inlinePatterns.add('update', UpdatePattern(UPDATE_RE, md), '_end')
         md.postprocessors.add('surround', SurroundProcessor(md), '_end')
diff --git a/bodhi/server/renderers.py b/bodhi/server/renderers.py
--- a/bodhi/server/renderers.py
+++ b/bodhi/server/renderers.py
@@ -128,7 +128,7 @@ def describe(*args, **kwargs):
                 for p in builds(*args, **kwargs):
                     text += f'* {p.nvr}\n'
                 text += f'## Update description:\n{notes(*args, **kwargs)}'
-                return markup(None, text)
+                return markup(None, text, bodhi=False)
             return describe
 
         getters = {
diff --git a/bodhi/server/util.py b/bodhi/server/util.py
--- a/bodhi/server/util.py
+++ b/bodhi/server/util.py
@@ -501,13 +501,14 @@ def hostname(context=None):
     return socket.gethostname()
 
 
-def markup(context, text):
+def markup(context, text, bodhi=True):
     """
     Return HTML from a markdown string.
 
     Args:
         context (mako.runtime.Context): Unused.
         text (str): Markdown text to be converted to HTML.
+        bodhi (bool): Enable or disable Bodhi markup extensions.
     Returns:
         str: HTML representation of the markdown text.
     """
@@ -542,8 +543,10 @@ def markup(context, text):
         "a",
     ]
 
-    markdown_text = markdown.markdown(text, extensions=['markdown.extensions.fenced_code',
-                                                        ffmarkdown.BodhiExtension()])
+    extensions = ['markdown.extensions.fenced_code', ]
+    if bodhi == True:
+        extensions.append(ffmarkdown.BodhiExtension())
+    markdown_text = markdown.markdown(text, extensions=extensions)
 
     # previously, we linkified text in ffmarkdown.py, but this was causing issues like #1721
     # so now we use the bleach linkifier to do this for us.


Test patch:
diff --git a/bodhi/tests/server/test_util.py b/bodhi/tests/server/test_util.py
--- a/bodhi/tests/server/test_util.py
+++ b/bodhi/tests/server/test_util.py
@@ -1091,6 +1091,21 @@ def test_markup_with_bleach_2(self, clean):
         clean.assert_called_once_with(expected_text, tags=expected_tags,
                                       attributes=expected_attributes)
 
+    def test_markup_without_bodhi_extensions(self):
+        """Ensure Bodhi extensions are not used with bodhi=False"""
+        text = (
+            'rhbz#12345\n'
+            '@mattia\n'
+            'FEDORA-EPEL-2019-1a2b3c4d5e'
+        )
+        html = util.markup(None, text, bodhi=False)
+        assert html == \
+            (
+                '<p>rhbz#12345\n'
+                '@mattia\n'
+                'FEDORA-EPEL-2019-1a2b3c4d5e</p>'
+            )
+
     def test_rpm_header(self):
         h = util.get_rpm_header('libseccomp')
         assert h['name'] == 'libseccomp'
diff --git a/bodhi/tests/server/views/test_generic.py b/bodhi/tests/server/views/test_generic.py
--- a/bodhi/tests/server/views/test_generic.py
+++ b/bodhi/tests/server/views/test_generic.py
@@ -259,6 +259,48 @@ def test_markdown_with_unknown_prefix_known_substring(self):
             "</div>"
         )
 
+    def test_markdown_with_update_alias(self):
+        """Update alias should be linkified."""
+        res = self.app.get('/markdown', {
+            'text': 'See FEDORA-2019-1a2b3c4d5e.',
+        }, status=200)
+        self.assertEqual(
+            res.json_body['html'],
+            '<div class="markdown">'
+            '<p>See '
+            '<a href="http://localhost/updates/FEDORA-2019-1a2b3c4d5e">'
+            'FEDORA-2019-1a2b3c4d5e'
+            '</a>.</p></div>'
+        )
+
+    def test_markdown_with_update_link(self):
+        """Update link should be converted to alias."""
+        res = self.app.get('/markdown', {
+            'text': 'See http://localhost:6543/updates/FEDORA-2019-1a2b3c4d5e.',
+        }, status=200)
+        self.assertEqual(
+            res.json_body['html'],
+            '<div class="markdown">'
+            '<p>See '
+            '<a href="http://localhost/updates/FEDORA-2019-1a2b3c4d5e">'
+            'FEDORA-2019-1a2b3c4d5e'
+            '</a>.</p></div>'
+        )
+
+    def test_markdown_with_fake_update_link(self):
+        """Update link of another domain isn't converted to alias."""
+        res = self.app.get('/markdown', {
+            'text': 'See http://bodhi.fake.org/updates/FEDORA-2019-1a2b3c4d5e.',
+        }, status=200)
+        self.assertEqual(
+            res.json_body['html'],
+            '<div class="markdown">'
+            '<p>See '
+            '<a href="http://bodhi.fake.org/updates/FEDORA-2019-1a2b3c4d5e">'
+            'http://bodhi.fake.org/updates/FEDORA-2019-1a2b3c4d5e'
+            '</a>.</p></div>'
+        )
+
     def test_markdown_with_fenced_code_block(self):
         res = self.app.get('/markdown', {
             'text': '```\nsudo dnf install bodhi\n```',