Repository: zulip/zulip

Issue:
Automatically shorten Github URLs
Currently, using [custom linkification filters](https://zulipchat.com/help/add-a-custom-linkification-filter) it's possible to use shortened forms to link to issues or pull requests. It would be nice if GitHub URLs could be automatically transformed into this shorter form. For example, if `https://github.com/org/repo/issues/1234` is used in a message, then it could be automatically shortened into `org/repo#1234` (or into a configured linkification pattern).
Often it is faster to just copy and paste links from Github however then these long URLs tend to clutter the messages so this feature would be nice to have for these cases.  


Discussion:
Hello @zulip/server-markdown members, this issue was labeled with the "area: markdown" label, so you may want to check it out!

<!-- areaLabelAddition -->
Yeah, this seems like a nice touch that we should add to what we're doing.  I think we might want to do it as a built-in feature of the markdown processor's link handler, not an optional linkification filter, since I don't see a reason anyone would want to turn this off.
@zulipbot claim
Welcome to Zulip, @scholtzan! We just sent you an invite to collaborate on this repository at https://github.com/zulip/zulip/invitations. Please accept this invite in order to claim this issue and begin a fun, rewarding experience contributing to Zulip!

Here's some tips to get you off to a good start:
* Join me on the [Zulip developers' server](https://chat.zulip.org), to get help, chat about this issue, and meet the other developers.
* Sign the [Dropbox Contributor License Agreement](https://opensource.dropbox.com/cla/), so that Zulip can use your code.
* [Unwatch this repository](https://help.github.com/articles/unwatching-repositories/), so that you don't get 100 emails a day.

As you work on this issue, you'll also want to refer to the [Zulip code contribution guide](https://zulip.readthedocs.io/en/latest/contributing/index.html), as well as the rest of the developer documentation on that site.

See you on the other side (that is, the pull request side)!
this sounds like a very nice addition!
maybe there could be a way to enable this also for self-hosted environments?
we are running a gitlab instance on premise so it would be extremely handy to configure that in the settings in order to get those shortened URL links...
Yeah, a GitLab version of this feature would require a realm-level setting for the GitLab server.  We can build the GitHub feature and then plan to extend it to support GitLab as well.
Hello @scholtzan, you have been unassigned from this issue because you have not updated this issue or any referenced pull requests for over 14 days.

You can reclaim this issue or claim any other issue by commenting `@zulipbot claim` on that issue.

Thanks for your contributions, and hope to see you again soon!
https://github.com/zulip/zulip/pull/11924

Resolving patch:
diff --git a/zerver/lib/markdown/__init__.py b/zerver/lib/markdown/__init__.py
--- a/zerver/lib/markdown/__init__.py
+++ b/zerver/lib/markdown/__init__.py
@@ -1625,9 +1625,65 @@ def __init__(self, compiled_re: Pattern[str], md: markdown.Markdown) -> None:
 
 
 class AutoLink(CompiledPattern):
+    """AutoLink takes care of linkifying link-format strings directly
+    present in the message (i.e. without markdown link syntax).  In
+    some hardcoded cases, it will rewrite the label to what the user
+    probably wanted if they'd taken the time to do so.
+    """
+
+    # Ideally, we'd use a dynamic commit prefix length based on the
+    # size of the repository, like Git itself does, but we're
+    # shortening Git commit IDs without looking at the corresponding
+    # repository.  It's not essential that the shortenings are
+    # globally unique as they're just shorthand, but 12 characters is
+    # the minimum to be unique for projects with about 1M commits.
+    COMMIT_ID_PREFIX_LENGTH = 12
+
+    def shorten_links(self, href: str) -> Optional[str]:
+        parts = urllib.parse.urlparse(href)
+        scheme, netloc, path, params, query, fragment = parts
+        if scheme == "https" and netloc in ["github.com"]:
+            # Split the path to extract our 4 variables.
+
+            # To do it cleanly without multiple if branches based on which of these
+            # variables are present, we here add a list of ["", "", ""...]
+            # to the result of path.split, which at worst can be []. We also remove
+            # the first empty string we'd get from "/foo/bar".split("/").
+
+            # Example path: "/foo/bar" output: ["foo", "bar", "", "", ""]
+            #         path: ""         output: ["", "", "", "", ""]
+            organisation, repository, artifact, value, remaining_path = (
+                path.split("/", 5)[1:] + [""] * 5
+            )[:5]
+
+            # Decide what type of links to shorten.
+            if not organisation or not repository or not artifact or not value:
+                return None
+            repo_short_text = "{}/{}".format(organisation, repository)
+
+            if fragment or remaining_path:
+                # We only intend to shorten links for the basic issue, PR, and commit ones.
+                return None
+
+            if netloc == "github.com":
+                return self.shorten_github_links(artifact, repo_short_text, value)
+        return None
+
+    def shorten_github_links(
+        self, artifact: str, repo_short_text: str, value: str
+    ) -> Optional[str]:
+        if artifact in ["pull", "issues"]:
+            return "{}#{}".format(repo_short_text, value)
+        if artifact == "commit":
+            return "{}@{}".format(repo_short_text, value[0 : self.COMMIT_ID_PREFIX_LENGTH])
+        return None
+
     def handleMatch(self, match: Match[str]) -> ElementStringNone:
         url = match.group("url")
         db_data = self.md.zulip_db_data
+        shortened_text = self.shorten_links(url)
+        if shortened_text is not None:
+            return url_to_a(db_data, url, shortened_text)
         return url_to_a(db_data, url)
 
 


Test patch:
diff --git a/zerver/tests/fixtures/markdown_test_cases.json b/zerver/tests/fixtures/markdown_test_cases.json
--- a/zerver/tests/fixtures/markdown_test_cases.json
+++ b/zerver/tests/fixtures/markdown_test_cases.json
@@ -932,6 +932,121 @@
       "input": "<h1>*<h1>[<h2>Static types in Python</h2>](https://blog.zulip.com/2016/10/13/static-types-in-python-oh-mypy)</h1>*</h1>",
       "expected_output": "<p>&lt;h1&gt;<em>&lt;h1&gt;<a href=\"https://blog.zulip.com/2016/10/13/static-types-in-python-oh-mypy\">&lt;h2&gt;Static types in Python&lt;/h2&gt;</a>&lt;/h1&gt;</em>&lt;/h1&gt;</p>",
       "marked_expected_output": "<p>&lt;h1&gt;<em>&lt;h1&gt;<a href=\"https://blog.zulip.com/2016/10/13/static-types-in-python-oh-mypy\">&lt;h2&gt;Static types in Python&lt;/h2&gt;</a>&lt;/h1&gt;</em>&lt;/h1&gt;\n\n</p>"
+    },
+    {
+      "name": "auto_shorten_github_repo_link",
+      "input": "https://github.com/zulip/zulip-mobile",
+      "expected_output": "<p><a href=\"https://github.com/zulip/zulip-mobile\">https://github.com/zulip/zulip-mobile</a></p>"
+    },
+    {
+      "name": "auto_shorten_github_issues_link",
+      "input": "https://github.com/zulip/zulip-mobile/issues/11895",
+      "expected_output": "<p><a href=\"https://github.com/zulip/zulip-mobile/issues/11895\">zulip/zulip-mobile#11895</a></p>"
+    },
+    {
+      "name": "auto_shorten_github_pull_link",
+      "input": "https://github.com/zulip/zulip-mobile/pull/16665",
+      "expected_output": "<p><a href=\"https://github.com/zulip/zulip-mobile/pull/16665\">zulip/zulip-mobile#16665</a></p>"
+    },
+    {
+      "name": "auto_shorten_github_commit_link",
+      "input": "https://github.com/zulip/zulip-mobile/commit/620e9cbf72ca729534aba41693d7ab2872caa394",
+      "expected_output": "<p><a href=\"https://github.com/zulip/zulip-mobile/commit/620e9cbf72ca729534aba41693d7ab2872caa394\">zulip/zulip-mobile@620e9cbf72ca</a></p>"
+    },
+    {
+      "name": "auto_shorten_github_commits_link",
+      "input": "https://github.com/zulip/zulip-mobile/pull/16860/commits/19e96bcea616e6e9b1a3c10f25930ac1f75d4f92",
+      "expected_output": "<p><a href=\"https://github.com/zulip/zulip-mobile/pull/16860/commits/19e96bcea616e6e9b1a3c10f25930ac1f75d4f92\">https://github.com/zulip/zulip-mobile/pull/16860/commits/19e96bcea616e6e9b1a3c10f25930ac1f75d4f92</a></p>"
+    },
+    {
+      "name": "auto_shorten_github_pull_initial_comment_link",
+      "input": "https://github.com/zulip/zulip-mobile/pull/16665#issue-513297835",
+      "expected_output": "<p><a href=\"https://github.com/zulip/zulip-mobile/pull/16665#issue-513297835\">https://github.com/zulip/zulip-mobile/pull/16665#issue-513297835</a></p>"
+    },
+    {
+      "name": "auto_shorten_github_pull_comment_link",
+      "input": "https://github.com/zulip/zulip-mobile/pull/16665#issuecomment-719814618",
+      "expected_output": "<p><a href=\"https://github.com/zulip/zulip-mobile/pull/16665#issuecomment-719814618\">https://github.com/zulip/zulip-mobile/pull/16665#issuecomment-719814618</a></p>"
+    },
+    {
+      "name": "auto_shorten_github_issues_initial_comment_link",
+      "input": "https://github.com/zulip/zulip-mobile/issues/16579#issue-725908927",
+      "expected_output": "<p><a href=\"https://github.com/zulip/zulip-mobile/issues/16579#issue-725908927\">https://github.com/zulip/zulip-mobile/issues/16579#issue-725908927</a></p>"
+    },
+    {
+      "name": "auto_shorten_github_issues_comment_link",
+      "input": "https://github.com/zulip/zulip-mobile/issues/16482#issuecomment-726354516",
+      "expected_output": "<p><a href=\"https://github.com/zulip/zulip-mobile/issues/16482#issuecomment-726354516\">https://github.com/zulip/zulip-mobile/issues/16482#issuecomment-726354516</a></p>"
+    },
+    {
+      "name": "auto_shorten_github_files_link",
+      "input": "https://github.com/zulip/zulip-mobile/pull/16860/files",
+      "expected_output": "<p><a href=\"https://github.com/zulip/zulip-mobile/pull/16860/files\">https://github.com/zulip/zulip-mobile/pull/16860/files</a></p>"
+    },
+    {
+      "name": "auto_shorten_github_files_comment_link",
+      "input": "https://github.com/zulip/zulip-mobile/pull/16860/files#r539133612",
+      "expected_output": "<p><a href=\"https://github.com/zulip/zulip-mobile/pull/16860/files#r539133612\">https://github.com/zulip/zulip-mobile/pull/16860/files#r539133612</a></p>"
+    },
+    {
+      "name": "auto_shorten_github_repo_issues_link",
+      "input": "https://github.com/zulip/zulip-mobile/issues",
+      "expected_output": "<p><a href=\"https://github.com/zulip/zulip-mobile/issues\">https://github.com/zulip/zulip-mobile/issues</a></p>"
+    },
+    {
+      "name": "auto_shorten_github_repo_pull_link",
+      "input": "https://github.com/zulip/zulip-mobile/pull",
+      "expected_output": "<p><a href=\"https://github.com/zulip/zulip-mobile/pull\">https://github.com/zulip/zulip-mobile/pull</a></p>"
+    },
+    {
+      "name": "auto_shorten_github_repo_commit_link",
+      "input": "https://github.com/zulip/zulip-mobile/commit",
+      "expected_output": "<p><a href=\"https://github.com/zulip/zulip-mobile/commit\">https://github.com/zulip/zulip-mobile/commit</a></p>"
+    },
+    {
+      "name": "auto_shorten_github_repo_clone_link",
+      "input": "https://github.com/zulip/zulip-mobile.git",
+      "expected_output": "<p><a href=\"https://github.com/zulip/zulip-mobile.git\">https://github.com/zulip/zulip-mobile.git</a></p>"
+    },
+    {
+      "name": "auto_shorten_github_repo_labels_link",
+      "input": "https://github.com/zulip/zulip-mobile/labels",
+      "expected_output": "<p><a href=\"https://github.com/zulip/zulip-mobile/labels\">https://github.com/zulip/zulip-mobile/labels</a></p>"
+    },
+    {
+      "name": "auto_shorten_github_labels_link",
+      "input": "https://github.com/zulip/zulip-mobile/labels/area%3A%20markdown",
+      "expected_output": "<p><a href=\"https://github.com/zulip/zulip-mobile/labels/area%3A%20markdown\">https://github.com/zulip/zulip-mobile/labels/area%3A%20markdown</a></p>"
+    },
+    {
+      "name": "auto_shorten_github_tree_link",
+      "input": "https://github.com/zulip/zulip-mobile/tree/chat.zulip.org",
+      "expected_output": "<p><a href=\"https://github.com/zulip/zulip-mobile/tree/chat.zulip.org\">https://github.com/zulip/zulip-mobile/tree/chat.zulip.org</a></p>"
+    },
+    {
+      "name": "auto_shorten_github_marketplace_circleci_link",
+      "input": "https://github.com/marketplace/circleci",
+      "expected_output": "<p><a href=\"https://github.com/marketplace/circleci\">https://github.com/marketplace/circleci</a></p>"
+    },
+    {
+      "name": "auto_shorten_github_foo_bar_baz_link",
+      "input": "https://github.com/foo/bar/baz",
+      "expected_output": "<p><a href=\"https://github.com/foo/bar/baz\">https://github.com/foo/bar/baz</a></p>"
+    },
+    {
+      "name": "auto_shorten_github_foo_bar_baz_zar_link",
+      "input": "https://github.com/foo/bar/baz/zar",
+      "expected_output": "<p><a href=\"https://github.com/foo/bar/baz/zar\">https://github.com/foo/bar/baz/zar</a></p>"
+    },
+    {
+      "name": "auto_shorten_github_marketplace_link",
+      "input": "https://github.com/marketplace",
+      "expected_output": "<p><a href=\"https://github.com/marketplace\">https://github.com/marketplace</a></p>"
+    },
+    {
+      "name": "auto_shorten_github_link",
+      "input": "https://github.com",
+      "expected_output": "<p><a href=\"https://github.com\">https://github.com</a></p>"
     }
   ],
   "linkify_tests": [