Преглед изворни кода

fix(backup): parse subpath-hosted Gitea/Forgejo repository URLs (#2642)

Gitea/Forgejo served under a ROOT_URL path prefix (e.g. https://host/gitea)
place repos at /<prefix>/owner/repo. The Gitea backend assumed a host-root
layout: parse_repo_url required exactly two path segments (so subpath URLs
raised "Cannot parse repository URL") and get_api_base dropped the prefix,
yielding https://host/api/v1 instead of https://host/gitea/api/v1.

Treat the final two path segments as owner/repo and keep leading segments as
a base-path prefix; derive the API base as {scheme}://{host}{prefix}/api/v1.
Root-hosted instances are unaffected. Forgejo inherits the fix; GitHub/GitLab
are untouched.
maziggy пре 1 месец
родитељ
комит
da33ba64d1
3 измењених фајлова са 44 додато и 6 уклоњено
  1. 1 0
      CHANGELOG.md
  2. 13 6
      backend/app/services/git_providers/gitea.py
  3. 30 0
      backend/tests/unit/test_git_providers.py

Разлика између датотеке није приказан због своје велике величине
+ 1 - 0
CHANGELOG.md


+ 13 - 6
backend/app/services/git_providers/gitea.py

@@ -63,14 +63,21 @@ class GiteaBackend(GitHubBackend):
             return tree_node.get("sha")
             return tree_node.get("sha")
         return None
         return None
 
 
+    # Gitea/Forgejo can be hosted under a URL path prefix (ROOT_URL like
+    # https://host/gitea), so the repo lives at /<prefix...>/<owner>/<repo>
+    # rather than at the host root (#2642). Capture the scheme+host+prefix as
+    # one group and the final two path segments as owner/repo; the lazy prefix
+    # group is empty for a root-hosted instance. One shared pattern keeps
+    # parse_repo_url() and get_api_base() from drifting.
+    _HTTPS_REPO_RE = re.compile(
+        r"(https?://[\w.\-]+(?::\d+)?(?:/[\w.\-]+)*?)/([\w.\-]{1,100})/([\w.\-]{1,100})(?:\.git)?/?$"
+    )
+
     def parse_repo_url(self, url: str) -> tuple[str, str]:
     def parse_repo_url(self, url: str) -> tuple[str, str]:
         """Return (owner, repo) — accepts both https:// and http:// for self-hosted instances."""
         """Return (owner, repo) — accepts both https:// and http:// for self-hosted instances."""
         if not url or len(url) > 500:
         if not url or len(url) > 500:
             raise ValueError("Invalid Git URL: URL too long or empty")
             raise ValueError("Invalid Git URL: URL too long or empty")
-        match = re.match(
-            r"https?://[\w.\-]+(:\d+)?/([\w.\-]{1,100})/([\w.\-]{1,100})(?:\.git)?/?$",
-            url,
-        )
+        match = self._HTTPS_REPO_RE.match(url)
         if match:
         if match:
             return match.group(2), match.group(3).removesuffix(".git")
             return match.group(2), match.group(3).removesuffix(".git")
         match = re.match(
         match = re.match(
@@ -82,8 +89,8 @@ class GiteaBackend(GitHubBackend):
         raise ValueError(f"Cannot parse repository URL: {url}")
         raise ValueError(f"Cannot parse repository URL: {url}")
 
 
     def get_api_base(self, repo_url: str) -> str:
     def get_api_base(self, repo_url: str) -> str:
-        """Derive API base from the repository URL's scheme and host."""
-        match = re.match(r"(https?://[\w.\-]+(:\d+)?)/", repo_url)
+        """Derive API base from the repository URL's scheme, host and any path prefix."""
+        match = self._HTTPS_REPO_RE.match(repo_url)
         if match:
         if match:
             return f"{match.group(1)}/api/v1"
             return f"{match.group(1)}/api/v1"
         raise ValueError(f"Cannot derive API base from URL: {repo_url}")
         raise ValueError(f"Cannot derive API base from URL: {repo_url}")

+ 30 - 0
backend/tests/unit/test_git_providers.py

@@ -463,6 +463,26 @@ class TestGiteaBackendApiBase:
         assert owner == "owner"
         assert owner == "owner"
         assert repo == "repo"
         assert repo == "repo"
 
 
+    def test_parse_url_subpath_hosted(self):
+        # Gitea under a ROOT_URL path prefix, e.g. https://host/gitea (#2642)
+        owner, repo = self.backend.parse_repo_url("https://DOMAIN/gitea/user/repo")
+        assert owner == "user"
+        assert repo == "repo"
+
+    def test_parse_url_subpath_hosted_with_git_suffix(self):
+        owner, repo = self.backend.parse_repo_url("https://DOMAIN/gitea/user/repo.git")
+        assert owner == "user"
+        assert repo == "repo"
+
+    def test_derives_api_base_subpath_hosted(self):
+        # API base must keep the path prefix so calls hit /gitea/api/v1 (#2642)
+        result = self.backend.get_api_base("https://DOMAIN/gitea/user/repo")
+        assert result == "https://DOMAIN/gitea/api/v1"
+
+    def test_derives_api_base_subpath_hosted_with_port(self):
+        result = self.backend.get_api_base("https://DOMAIN:3000/gitea/user/repo")
+        assert result == "https://DOMAIN:3000/gitea/api/v1"
+
 
 
 class TestGiteaBackendPushFiles:
 class TestGiteaBackendPushFiles:
     def setup_method(self):
     def setup_method(self):
@@ -1358,6 +1378,16 @@ class TestForgejoBackendApiBase:
         assert owner == "owner"
         assert owner == "owner"
         assert repo == "repo"
         assert repo == "repo"
 
 
+    def test_parse_url_subpath_hosted(self):
+        # Forgejo inherits GiteaBackend's subpath handling (#2642)
+        owner, repo = self.backend.parse_repo_url("https://DOMAIN/forgejo/user/repo")
+        assert owner == "user"
+        assert repo == "repo"
+
+    def test_derives_api_base_subpath_hosted(self):
+        result = self.backend.get_api_base("https://DOMAIN/forgejo/user/repo")
+        assert result == "https://DOMAIN/forgejo/api/v1"
+
 
 
 class TestForgejoTestConnection:
 class TestForgejoTestConnection:
     """ForgejoBackend overrides test_connection to handle Forgejo v15+ 404-not-403 behaviour."""
     """ForgejoBackend overrides test_connection to handle Forgejo v15+ 404-not-403 behaviour."""

Неке датотеке нису приказане због велике количине промена