Repository: PostHog/posthog
Pull request: Support plugins installed from GitLab
## Changes

- Adds support for downloading ZIP files from various GitLab URLs
- Closes #2940
- **Does not** yet add support for private repos (#2941)

## Checklist

- [ ] All querysets/queries filter by Organization, by Team, and by User
- [ ] Django backend tests
- [ ] Jest frontend tests
- [ ] Cypress end-to-end tests


Problem statement:
GitLab Plugins
## Is your feature request related to a problem?

You can currently install plugins from Github and NPM sources, but not GitLab

## Describe the solution you'd like

Copy a GitLab URL to install a plugin

## Describe alternatives you've considered

Using the plugin source editor.

## Additional context

Came from a conversation in the Users' slack

#### *Thank you* for your feature request – we love each and every one!

Hints from issue discussion:
Yes, we have on prem instance of GitLab and would like to install the plugins from it so this would be most welcome! :)

Commit message: support gitlab urls

Patch to review:
diff --git a/posthog/plugins/test/mock.py b/posthog/plugins/test/mock.py
index d3cdb4828f9e6..41fddc4017c08 100644
--- a/posthog/plugins/test/mock.py
+++ b/posthog/plugins/test/mock.py
@@ -4,6 +4,7 @@
 from .plugin_archives import (
     HELLO_WORLD_PLUGIN_GITHUB_ATTACHMENT_ZIP,
     HELLO_WORLD_PLUGIN_GITHUB_ZIP,
+    HELLO_WORLD_PLUGIN_GITLAB_ZIP,
     HELLO_WORLD_PLUGIN_NPM_TGZ,
 )
 
@@ -63,6 +64,11 @@ def ok(self):
     ):
         return MockBase64Response(HELLO_WORLD_PLUGIN_GITHUB_ATTACHMENT_ZIP[1], 200)
 
+    if args[0] == "https://gitlab.com/mariusandra/helloworldplugin/-/archive/{}/helloworldplugin-{}.zip".format(
+        HELLO_WORLD_PLUGIN_GITLAB_ZIP[0], HELLO_WORLD_PLUGIN_GITLAB_ZIP[0]
+    ):
+        return MockBase64Response(HELLO_WORLD_PLUGIN_GITLAB_ZIP[1], 200)
+
     if args[0] == "https://registry.npmjs.org/posthog-helloworld-plugin/-/posthog-helloworld-plugin-0.0.0.tgz":
         return MockBase64Response(HELLO_WORLD_PLUGIN_NPM_TGZ[1], 200)
 
diff --git a/posthog/plugins/test/plugin_archives.py b/posthog/plugins/test/plugin_archives.py
index d2e80e743b004..f83cbf40d8c87 100644
--- a/posthog/plugins/test/plugin_archives.py
+++ b/posthog/plugins/test/plugin_archives.py
@@ -11,3 +11,7 @@
     "0.0.1",
     "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",
 )
+HELLO_WORLD_PLUGIN_GITLAB_ZIP = (
+    "ff78cbe1d70316055c610a962a8355a4616d874b",
+    "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NjA1NWM2MTBhOTYyYTgzNTVhNDYxNmQ4NzRiL1JFQURNRS5tZFVUBQABdUWZX1BLAQIAAAoAAAAIAE5SXFF+xp1y5gAAAOwBAABCAAkAAAAAAAEAAAAAAMIIAABoZWxsb3dvcmxkcGx1Z2luLWZmNzhjYmUxZDcwMzE2MDU1YzYxMGE5NjJhODM1NWE0NjE2ZDg3NGIvaW5kZXguanNVVAUAAXVFmV9QSwECAAAKAAAACABOUlxRmBG7bDAAAAA6AAAAQAAJAAAAAAABAAAAAAARCgAAaGVsbG93b3JsZHBsdWdpbi1mZjc4Y2JlMWQ3MDMxNjA1NWM2MTBhOTYyYTgzNTVhNDYxNmQ4NzRiL2xpYi5qc1VUBQABdUWZX1BLAQIAAAoAAAAIAE5SXFG6/5wJ0AAAAFEBAABFAAkAAAAAAAEAAAAAAKgKAABoZWxsb3dvcmxkcGx1Z2luLWZmNzhjYmUxZDcwMzE2MDU1YzYxMGE5NjJhODM1NWE0NjE2ZDg3NGIvcGx1Z2luLmpzb25VVAUAAXVFmV9QSwUGAAAAAAgACADGAwAA5AsAACgAZmY3OGNiZTFkNzAzMTYwNTVjNjEwYTk2MmE4MzU1YTQ2MTZkODc0Yg==",
+)
diff --git a/posthog/plugins/test/test_utils.py b/posthog/plugins/test/test_utils.py
index 7fb55e5583217..fc179fd10232c 100644
--- a/posthog/plugins/test/test_utils.py
+++ b/posthog/plugins/test/test_utils.py
@@ -5,12 +5,12 @@
 from posthog.test.base import BaseTest
 
 from .mock import mocked_plugin_requests_get
-from .plugin_archives import HELLO_WORLD_PLUGIN_GITHUB_ZIP, HELLO_WORLD_PLUGIN_NPM_TGZ
+from .plugin_archives import HELLO_WORLD_PLUGIN_GITHUB_ZIP, HELLO_WORLD_PLUGIN_GITLAB_ZIP, HELLO_WORLD_PLUGIN_NPM_TGZ
 
 
 @mock.patch("requests.get", side_effect=mocked_plugin_requests_get)
 class TestPluginsUtils(BaseTest):
-    def test_parse_urls(self, mock_get):
+    def test_parse_github_urls(self, mock_get):
         parsed_url = parse_url("https://github.com/PostHog/posthog")
         self.assertEqual(parsed_url["type"], "github")
         self.assertEqual(parsed_url["user"], "PostHog")
@@ -67,6 +67,37 @@ def test_parse_urls(self, mock_get):
         self.assertEqual(parsed_url["repo"], "posthog")
         self.assertEqual(parsed_url["tag"], "82c9218ee40f561b7f37a22d6b6a0ca82887ee3e")
 
+    def test_parse_gitlab_urls(self, mock_get):
+        parsed_url = parse_url("https://gitlab.com/mariusandra/helloworldplugin")
+        self.assertEqual(parsed_url["type"], "gitlab")
+        self.assertEqual(parsed_url["project"], "mariusandra/helloworldplugin")
+        self.assertEqual(parsed_url.get("tag", None), None)
+
+        parsed_url = parse_url(
+            "https://gitlab.com/gitlab-org/gl-openshift/openshift-demos/openshift-custom-pipeline/-/tree/master"
+        )
+        self.assertEqual(parsed_url["project"], "gitlab-org/gl-openshift/openshift-demos/openshift-custom-pipeline")
+        self.assertEqual(parsed_url["tag"], "master")
+
+        parsed_url = parse_url(
+            "https://gitlab.com/gitlab-org/gl-openshift/openshift-demos/openshift-custom-pipeline/-/tree/2b6494bdf8ad35073aafe36ca8a1bdfaf3dc72d1"
+        )
+        self.assertEqual(parsed_url["project"], "gitlab-org/gl-openshift/openshift-demos/openshift-custom-pipeline")
+        self.assertEqual(parsed_url["tag"], "2b6494bdf8ad35073aafe36ca8a1bdfaf3dc72d1")
+
+        parsed_url = parse_url(
+            "https://gitlab.com/gitlab-org/gl-openshift/openshift-demos/openshift-custom-pipeline/-/commit/2b6494bdf8ad35073aafe36ca8a1bdfaf3dc72d1"
+        )
+        self.assertEqual(parsed_url["project"], "gitlab-org/gl-openshift/openshift-demos/openshift-custom-pipeline")
+        self.assertEqual(parsed_url["tag"], "2b6494bdf8ad35073aafe36ca8a1bdfaf3dc72d1")
+
+        parsed_url = parse_url(
+            "https://gitlab.com/gitlab-org/gl-openshift/openshift-demos/openshift-custom-pipeline/-/archive/master/openshift-custom-pipeline-master.zip"
+        )
+        self.assertEqual(parsed_url["project"], "gitlab-org/gl-openshift/openshift-demos/openshift-custom-pipeline")
+        self.assertEqual(parsed_url["tag"], "master")
+
+    def test_parse_npm_urls(self, mock_get):
         parsed_url = parse_url("https://www.npmjs.com/package/posthog-helloworld-plugin")
         self.assertEqual(parsed_url["type"], "npm")
         self.assertEqual(parsed_url["pkg"], "posthog-helloworld-plugin")
@@ -89,7 +120,7 @@ def test_parse_urls(self, mock_get):
         self.assertEqual(parsed_url["pkg"], "posthog-helloworld-plugin")
         self.assertEqual(parsed_url["version"], "0.0.0")
 
-    def test_download_plugin_archive(self, mock_get):
+    def test_download_plugin_archive_github(self, mock_get):
         plugin_github_zip_1 = download_plugin_archive(
             "https://www.github.com/PostHog/helloworldplugin/commit/82c9218ee40f561b7f37a22d6b6a0ca82887ee3e",
             HELLO_WORLD_PLUGIN_GITHUB_ZIP[0],
@@ -101,6 +132,14 @@ def test_download_plugin_archive(self, mock_get):
         )
         self.assertEqual(plugin_github_zip_2, base64.b64decode(HELLO_WORLD_PLUGIN_GITHUB_ZIP[1]))
 
+    def test_download_plugin_archive_gitlab(self, mock_get):
+        plugin_gitlab = download_plugin_archive(
+            "https://www.gitlab.com/mariusandra/helloworldplugin/-/commit/ff78cbe1d70316055c610a962a8355a4616d874b",
+            HELLO_WORLD_PLUGIN_GITLAB_ZIP[0],
+        )
+        self.assertEqual(plugin_gitlab, base64.b64decode(HELLO_WORLD_PLUGIN_GITLAB_ZIP[1]))
+
+    def test_download_plugin_archive_npm(self, mock_get):
         plugin_npm_tgz = download_plugin_archive("https://www.npmjs.com/package/posthog-helloworld-plugin/v/0.0.0")
         self.assertEqual(plugin_npm_tgz, base64.b64decode(HELLO_WORLD_PLUGIN_NPM_TGZ[1]))
 
@@ -110,6 +149,11 @@ def test_get_json_from_archive(self, mock_get):
         self.assertEqual(plugin_json_zip["url"], "https://github.com/PostHog/helloworldplugin")
         self.assertEqual(plugin_json_zip["description"], "Greet the World and Foo a Bar, JS edition!")
 
+        plugin_json_zip = get_json_from_archive(base64.b64decode(HELLO_WORLD_PLUGIN_GITLAB_ZIP[1]), "plugin.json")
+        self.assertEqual(plugin_json_zip["name"], "hellojsplugin")
+        self.assertEqual(plugin_json_zip["url"], "https://github.com/PosthHog/helloworldplugin")
+        self.assertEqual(plugin_json_zip["description"], "Greet the World and Foo a Bar, JS edition!")
+
         plugin_json_tgz = get_json_from_archive(base64.b64decode(HELLO_WORLD_PLUGIN_NPM_TGZ[1]), "plugin.json")
         self.assertEqual(plugin_json_tgz["name"], "helloworldplugin")
         self.assertEqual(plugin_json_tgz["url"], "https://github.com/PostHog/helloworldplugin")
diff --git a/posthog/plugins/utils.py b/posthog/plugins/utils.py
index 96486a8f6c5db..da70e570489d5 100644
--- a/posthog/plugins/utils.py
+++ b/posthog/plugins/utils.py
@@ -4,6 +4,7 @@
 import re
 import tarfile
 from typing import Dict, Optional
+from urllib.parse import quote
 from zipfile import BadZipFile, ZipFile
 
 import requests
@@ -38,6 +39,36 @@ def parse_github_url(url: str, get_latest_if_none=False) -> Optional[Dict[str, s
     return parsed
 
 
+def parse_gitlab_url(url: str, get_latest_if_none=False) -> Optional[Dict[str, str]]:
+    url = url.strip("/")
+    match = re.search(r"^https?:\/\/(?:www\.)?gitlab\.com\/([A-Za-z0-9_.\-\/]+)$", url,)
+    if not match:
+        return None
+    project = match.group(1)
+    if "/-/" in project:
+        parsed = {
+            "type": "gitlab",
+            "project": match.group(1).split("/-/")[0],
+            "tag": project.split("/-/")[1].split("/")[1],
+        }
+    else:
+        parsed = {"type": "gitlab", "project": match.group(1), "tag": None}
+
+    parsed["root_url"] = "https://gitlab.com/{}".format(parsed["project"])
+    if get_latest_if_none and not parsed["tag"]:
+        try:
+            commits_url = "https://gitlab.com/api/v4/projects/{}/repository/commits".format(quote(parsed["project"]))
+            commits = requests.get(commits_url).json()
+            if len(commits) > 0 and commits[0].get("web_url", None):
+                return parse_url(commits[0]["web_url"])
+            raise
+        except Exception:
+            raise Exception("Could not get latest commit for: {}".format(parsed["root_url"]))
+    if parsed["tag"]:
+        parsed["tagged_url"] = "https://gitlab.com/{}/-/tree/{}".format(parsed["project"], parsed["tag"])
+    return parsed
+
+
 def parse_npm_url(url: str, get_latest_if_none=False) -> Optional[Dict[str, str]]:
     match = re.search(r"^https?:\/\/(?:www\.)?npmjs\.com\/package\/([a-z0-9_-]+)\/?(v\/([A-Za-z0-9_.-]+)\/?|)$", url)
     if not match:
@@ -60,6 +91,9 @@ def parse_url(url: str, get_latest_if_none=False) -> Dict[str, str]:
     if parsed_url:
         return parsed_url
     parsed_url = parse_npm_url(url, get_latest_if_none)
+    if parsed_url:
+        return parsed_url
+    parsed_url = parse_gitlab_url(url, get_latest_if_none)
     if parsed_url:
         return parsed_url
     raise Exception("Must be a Github Repository or NPM package URL!")
@@ -75,6 +109,12 @@ def download_plugin_archive(url: str, tag: Optional[str] = None):
         url = "https://github.com/{user}/{repo}/archive/{tag}.zip".format(
             user=parsed_url["user"], repo=parsed_url["repo"], tag=tag or parsed_url["tag"]
         )
+    elif parsed_url["type"] == "gitlab":
+        if not (tag or parsed_url.get("tag", None)):
+            raise Exception("No Github tag given!")
+        url = "https://gitlab.com/{project}/-/archive/{tag}/{repo}-{tag}.zip".format(
+            project=parsed_url["project"], repo=parsed_url["project"].split("/")[-1], tag=tag or parsed_url["tag"]
+        )
     elif parsed_url["type"] == "npm":
         if not (tag or parsed_url.get("version", None)):
             raise Exception("No NPM version given")


Code review comments:
diff_hunk: @@ -75,6 +109,12 @@ def download_plugin_archive(url: str, tag: Optional[str] = None):
         url = "https://github.com/{user}/{repo}/archive/{tag}.zip".format(
             user=parsed_url["user"], repo=parsed_url["repo"], tag=tag or parsed_url["tag"]
         )
+    elif parsed_url["type"] == "gitlab":
+        if not (tag or parsed_url.get("tag", None)):
+            raise Exception("No Github tag given!")
path: posthog/plugins/utils.py
text: @user1:
GitLab?