Repository: relekang/python-semantic-release
Pull request #282 (2020-12-05 00:12:14)

feat(changelog): add PR links in markdown
GitHub release notes automagically link to the PR, but changelog markdown doesn't. Replace (#123) at the end of a message with a markdown link.

wyardley: Open to suggestions on style / formatting here. I decided to do the f string for the table formatting in two lines, and to put the spaces at the beginning of the next line for the tests, but not super married to either way of doing it.

Similarly, could easily make the regex part little terser or change stuff there if it would be preferred.
wyardley: Also, we may want to (fairly soon) update the gitlab ones to follow the same hostname as for the API, assuming that's the correct behavior, but that would probably go along with the changes I mentioned in #277 

Patch:
diff --git a/semantic_release/changelog/changelog.py b/semantic_release/changelog/changelog.py
index 6edd22a..453f241 100644
--- a/semantic_release/changelog/changelog.py
+++ b/semantic_release/changelog/changelog.py
@@ -1,8 +1,32 @@
+import re
+
 from typing import Iterable, Optional
 
 from ..settings import config
 
 
+def add_pr_link(owner: str, repo_name: str, message: str) -> str:
+    """
+    GitHub release notes automagically link to the PR, but changelog markdown
+    doesn't. Replace (#123) at the end of a message with a markdown link.
+    """
+
+    pr_pattern = re.compile(r"\s+\(#(\d{1,8})\)$")
+    match = re.search(pr_pattern, message)
+
+    if match:
+        pr_number = match.group(1)
+        url = (
+            f"https://gitlab.com/{owner}/{repo_name}/-/merge_requests/{pr_number}"
+            if config.get("hvcs") == "gitlab"
+            else f"https://github.com/{owner}/{repo_name}/pull/{pr_number}"
+        )
+
+        return re.sub(pr_pattern, f" ([#{pr_number}]({url}))", message)
+
+    return message
+
+
 def get_changelog_sections(changelog: dict, changelog_sections: list) -> Iterable[str]:
     """Generator which yields each changelog section to be included"""
 
@@ -35,7 +59,8 @@ def changelog_headers(
 
         # Add each commit from the section in an unordered list
         for item in changelog[section]:
-            output += f"* {item[1]} ({get_hash_link(owner, repo_name, item[0])})\n"
+            message = add_pr_link(owner, repo_name, item[1])
+            output += f"* {message} ({get_hash_link(owner, repo_name, item[0])})\n"
 
     return output
 
@@ -48,7 +73,8 @@ def changelog_table(
     for section in get_changelog_sections(changelog, changelog_sections):
         items = "<br>".join(
             [
-                f"{item[1]} ({get_hash_link(owner, repo_name, item[0])})"
+                f"{add_pr_link(owner, repo_name, item[1])} "
+                f"({get_hash_link(owner, repo_name, item[0])})"
                 for item in changelog[section]
             ]
         )


Test patch:
diff --git a/tests/test_changelog.py b/tests/test_changelog.py
index eec838a..1134f95 100644
--- a/tests/test_changelog.py
+++ b/tests/test_changelog.py
@@ -1,9 +1,16 @@
 import mock
+import pytest
 
 from semantic_release.changelog import markdown_changelog
-from semantic_release.changelog.changelog import changelog_table, get_changelog_sections
+from semantic_release.changelog.changelog import (
+    changelog_table,
+    get_changelog_sections,
+    add_pr_link,
+)
 from semantic_release.changelog.compare import compare_url, get_github_compare_url
 
+from . import wrapped_config_get
+
 
 def test_markdown_changelog():
     assert markdown_changelog(
@@ -19,8 +26,8 @@ def test_markdown_changelog():
                 ("145", "Add non-breaking super-feature"),
                 ("134", "Add super-feature"),
             ],
-            "fix": [("234", "Fix bug in super-feature")],
-            "documentation": [("0", "Document super-feature")],
+            "fix": [("234", "Fix bug in super-feature (#15)")],
+            "documentation": [("0", "Document super-feature (#189)")],
             "performance": [],
         },
     ) == (
@@ -31,7 +38,8 @@ def test_markdown_changelog():
         "* Add super-feature ([`134`](https://github.com/owner/repo_name/commit/134))\n"
         "\n"
         "### Fix\n"
-        "* Fix bug in super-feature ([`234`](https://github.com/owner/repo_name/"
+        "* Fix bug in super-feature ([#15](https://github.com/owner/repo_name/pull/15))"
+        " ([`234`](https://github.com/owner/repo_name/"
         "commit/234))\n"
         "\n"
         "### Breaking\n"
@@ -39,17 +47,52 @@ def test_markdown_changelog():
         " ([`21`](https://github.com/owner/repo_name/commit/21))\n"
         "\n"
         "### Documentation\n"
-        "* Document super-feature ([`0`](https://github.com/owner/repo_name/commit/0))"
+        "* Document super-feature ([#189](https://github.com/owner/repo_name/pull/189))"
+        " ([`0`](https://github.com/owner/repo_name/commit/0))"
     )
 
 
+def test_markdown_changelog_gitlab():
+    with mock.patch(
+        "semantic_release.changelog.config.get", wrapped_config_get(hvcs="gitlab")
+    ):
+
+        assert markdown_changelog(
+            "owner",
+            "repo_name",
+            "0",
+            {
+                "refactor": [("12", "Refactor super-feature")],
+                "feature": [
+                    ("145", "Add non-breaking super-feature (#1)"),
+                    ("134", "Add super-feature"),
+                ],
+                "documentation": [("0", "Document super-feature (#189)")],
+                "performance": [],
+            },
+        ) == (
+            # Expected output with the default configuration
+            "### Feature\n"
+            "* Add non-breaking super-feature ([#1](https://gitlab.com/owner/"
+            "repo_name/-/merge_requests/1)) ([`145`](https://gitlab.com/owner/"
+            "repo_name/-/commit/145))\n"
+            "* Add super-feature ([`134`](https://gitlab.com/owner/repo_name/-/"
+            "commit/134))\n"
+            "\n"
+            "### Documentation\n"
+            "* Document super-feature ([#189](https://gitlab.com/owner/repo_name/"
+            "-/merge_requests/189)) ([`0`](https://gitlab.com/owner/repo_name/"
+            "-/commit/0))"
+        )
+
+
 def test_changelog_table():
     assert changelog_table(
         "owner",
         "repo_name",
         {
             "feature": [("sha1", "commit1"), ("sha2", "commit2")],
-            "fix": [("sha3", "commit3")],
+            "fix": [("sha3", "commit3 (#123)")],
         },
         ["section1", "section2"],
     ) == (
@@ -57,7 +100,8 @@ def test_changelog_table():
         "| --- | --- |\n"
         "| Feature | commit1 ([`sha1`](https://github.com/owner/repo_name/commit/sha1))"
         "<br>commit2 ([`sha2`](https://github.com/owner/repo_name/commit/sha2)) |\n"
-        "| Fix | commit3 ([`sha3`](https://github.com/owner/repo_name/commit/sha3)) |\n"
+        "| Fix | commit3 ([#123](https://github.com/owner/repo_name/pull/123))"
+        " ([`sha3`](https://github.com/owner/repo_name/commit/sha3)) |\n"
     )
 
 
@@ -101,6 +145,33 @@ def test_get_changelog_sections():
     )
 
 
+@pytest.mark.parametrize(
+    "message,hvcs,expected_output",
+    [
+        (
+            "test (#123)",
+            "github",
+            "test ([#123](https://github.com/owner/name/pull/123))",
+        ),
+        ("test without commit", "github", "test without commit"),
+        ("test (#123) in middle", "github", "test (#123) in middle"),
+        (
+            "test (#123)",
+            "gitlab",
+            "test ([#123](https://gitlab.com/owner/name/-/merge_requests/123))",
+        ),
+        ("test without commit", "gitlab", "test without commit"),
+    ],
+)
+def test_add_pr_link(message, hvcs, expected_output):
+
+    with mock.patch(
+        "semantic_release.changelog.config.get", wrapped_config_get(hvcs=hvcs)
+    ):
+
+        assert add_pr_link("owner", "name", message) == expected_output
+
+
 def test_github_compare_url():
     with mock.patch(
         "semantic_release.changelog.compare.get_repository_owner_and_name",