Repository: roskakori/pygount
Pull request #169 (2024-07-13 18:45:41)

#164 Transform common project URLs to repository
I added the previous issue i contributed as well


Patch:
diff --git a/docs/changes.rst b/docs/changes.rst
index 45de161..19a11c3 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -10,6 +10,10 @@ Version 3.0.0, 2025-05-23
 
 * Count pure markup files as documentation: (contributed by Tytus Bucholc, issue
   `#6 <https://github.com/roskakori/pygount/issues/6>`_).
+* Fix silent error on git failing: (contributed by Tom De Bièvre, issue
+  `#162 <https://github.com/roskakori/pygount/issues/162>`_)
+* Transform common project URLs to repository: (contributed by Tom De Bièvre, issue
+  `#164 <https://github.com/roskakori/pygount/issues/164>`_)
 * Change dependency rules for rich to be more lenient (suggested by Brian McGillion, issue
   `#193 <https://github.com/roskakori/pygount/issues/193>`_)
 
@@ -36,6 +40,8 @@ Version 2.0.0, 2025-03-16
   `#157 <https://github.com/roskakori/pygount/issues/157>`_).
 * Development: Change default branch to main (issue
   `#160 <https://github.com/roskakori/pygount/issues/160>`_).
+* Removed deprecated code: (contributed by Marco Gambone and Niels Vanden Bussche, issue
+  `#47 <https://github.com/roskakori/pygount/issues/47>`_).
 
 Version 1.8.0, 2024-05-13
 
diff --git a/pygount/analysis.py b/pygount/analysis.py
index aeb99c2..cfd2738 100644
--- a/pygount/analysis.py
+++ b/pygount/analysis.py
@@ -30,7 +30,10 @@ import pygount.xmldialect
 from pygount.common import mapped_repr
 from pygount.git_storage import GitStorage, git_remote_url_and_revision_if_any
 
-GIT_REPO_REGEX = re.compile(r"^(https?://|git@)")
+HTTP_URL_REGEX = re.compile(r"^(https?://)")
+_ALLOWED_GIT_PLATFORMS = ["github.com", "bitbucket.org", "gitlab.com"]
+_ALLOWED_GIT_PLATFORM_CHOICES_PATTERN = "|".join(map(re.escape, _ALLOWED_GIT_PLATFORMS))
+GIT_REPO_REGEX = re.compile(rf"^(https?://|git@)({_ALLOWED_GIT_PLATFORM_CHOICES_PATTERN})/[^/]+/[^/]+")
 
 # Attempt to import chardet.
 try:
@@ -49,7 +52,6 @@ DEFAULT_FOLDER_PATTERNS_TO_SKIP_TEXT = ", ".join(
     [".?*", "_svn", "__pycache__"]  # Subversion hack for Windows  # Python byte code
 )
 
-
 #: Pygments token type; we need to define our own type because pygments' ``_TokenType`` is internal.
 TokenType = type(pygments.token.Token)
 
@@ -648,30 +650,40 @@ class SourceScanner:
 
     def _source_paths_and_groups_to_analyze(self, source_patterns_to_analyze) -> list[PathData]:
         assert source_patterns_to_analyze is not None
+
         result = []
+
+        def _process_source_pattern(source_pattern: str):
+            remote_url, revision = git_remote_url_and_revision_if_any(source_pattern)
+            if remote_url is not None:
+                git_storage = GitStorage(remote_url, revision)
+                self._git_storages.append(git_storage)
+                git_storage.extract()
+                result.extend(
+                    self._paths_and_group_to_analyze(git_storage.temp_folder, tmp_dir=git_storage.temp_folder)
+                )
+            else:
+                has_url_prefix = re.match(HTTP_URL_REGEX, source_pattern)
+                if has_url_prefix:
+                    is_git_url = re.match(GIT_REPO_REGEX, source_pattern_to_analyze) is not None
+                    if not is_git_url:
+                        raise pygount.Error(
+                            f'URL to git repository {source_pattern} must end with ".git" or must match the pattern '
+                            f"http(s)://({'|'.join(_ALLOWED_GIT_PLATFORMS)})/<...>/<...>.git. "
+                            f"For example: git@github.com:roskakori/pygount.git or "
+                            f"https://github.com/roskakori/pygount.git."
+                        )
+                    source_pattern = source_pattern.rstrip("/")
+                    _process_source_pattern(source_pattern + ".git")
+                else:
+                    result.extend(self._paths_and_group_to_analyze(source_pattern_to_analyze))
+
         # NOTE: We could avoid initializing `source_pattern_to_analyze` here by moving the `try` inside
         #  the loop, but this would incor a performance overhead (ruff's PERF203).
         source_pattern_to_analyze = None
         try:
             for source_pattern_to_analyze in source_patterns_to_analyze:
-                remote_url, revision = git_remote_url_and_revision_if_any(source_pattern_to_analyze)
-                if remote_url is not None:
-                    git_storage = GitStorage(remote_url, revision)
-                    self._git_storages.append(git_storage)
-                    git_storage.extract()
-                    # TODO#113: Find a way to exclude the ugly temp folder from the source path.
-                    result.extend(
-                        self._paths_and_group_to_analyze(git_storage.temp_folder, tmp_dir=git_storage.temp_folder)
-                    )
-                else:
-                    git_url_match = re.match(GIT_REPO_REGEX, source_pattern_to_analyze)
-                    if git_url_match is not None:
-                        raise pygount.Error(
-                            'URL to git repository must end with ".git", for example '
-                            "git@github.com:roskakori/pygount.git or "
-                            "https://github.com/roskakori/pygount.git."
-                        )
-                    result.extend(self._paths_and_group_to_analyze(source_pattern_to_analyze))
+                _process_source_pattern(source_pattern_to_analyze)
         except OSError as error:
             assert source_pattern_to_analyze is not None
             raise OSError(f'cannot scan "{source_pattern_to_analyze}" for source files: {error}') from error


Test patch:
diff --git a/tests/test_analysis.py b/tests/test_analysis.py
index e816783..823f682 100644
--- a/tests/test_analysis.py
+++ b/tests/test_analysis.py
@@ -12,6 +12,7 @@ from io import BytesIO, StringIO
 import pytest
 from pygments import lexers, token
 
+import pygount
 from pygount import Error as PygountError
 from pygount import analysis, common
 from pygount.analysis import (
@@ -63,11 +64,20 @@ class SourceScannerTest(TempFolderTest):
         scanned_names = [os.path.basename(path_data.source_path) for path_data in scanner.source_paths()]
         assert scanned_names == [name_to_include]
 
-    def test_fails_on_non_repo_url(self):
+    def test_succeeds_on_not_git_extension(self):
         non_repo_urls = [["https://github.com/roskakori/pygount/"], ["git@github.com:roskakori/pygount"]]
         for non_repo_url in non_repo_urls:
-            with analysis.SourceScanner(non_repo_url) as scanner, pytest.raises(PygountError):
-                next(scanner.source_paths())
+            with analysis.SourceScanner(non_repo_url) as scanner:
+                _ = list(scanner.source_paths())
+
+    def test_fails_on_non_git_urls(self):
+        non_repo_urls = [["https://no/git/url"], ["https://google.com/nogit"]]
+        for non_repo_url in non_repo_urls:
+            with (
+                analysis.SourceScanner(non_repo_url) as scanner,
+                pytest.raises(pygount.Error, match="URL to git repository"),
+            ):
+                _ = list(scanner.source_paths())
 
     def test_can_find_python_files_in_dot(self):
         scanner = analysis.SourceScanner(["."], "py")