diff --git a/tests/unit/packaging/test_models.py b/tests/unit/packaging/test_models.py index baba5e2bef0a..3658d733b503 100644 --- a/tests/unit/packaging/test_models.py +++ b/tests/unit/packaging/test_models.py @@ -977,6 +977,52 @@ def test_verified_github_open_issueo_info_url_is_none_without_verified_url( release = DBReleaseFactory.create() assert release.verified_github_open_issue_info_url is None + @pytest.mark.parametrize( + ("url", "expected"), + [ + ( + "https://gitlab.com/someuser/someproject", + "someuser/someproject", + ), + ( + "https://gitlab.com/someuser/someproject/", + "someuser/someproject", + ), + ( + "https://gitlab.com/someuser/someproject/-/tree/stable-9", + "someuser/someproject", + ), + ( + "https://www.gitlab.com/someuser/someproject", + "someuser/someproject", + ), + ("https://gitlab.com/someuser/", None), + ("https://google.com/pypi/warehouse/tree/main", None), + ("https://google.com", None), + ("incorrect url", None), + ( + "https://gitlab.com/someuser/someproject.git", + "someuser/someproject", + ), + ( + "https://www.gitlab.com/someuser/someproject.git/", + "someuser/someproject", + ), + ("git@bitbucket.org:definex/dsgnutils.git", None), + ], + ) + def test_verified_gitlab_repository(self, db_session, url, expected): + release = DBReleaseFactory.create() + release.project_urls["Homepage"] = {"url": url, "verified": True} + assert release.verified_gitlab_repository == expected + + def test_verified_gitlab_repository_is_none_without_verified_url( + self, + db_session, + ): + release = DBReleaseFactory.create() + assert release.verified_gitlab_repository is None + def test_trusted_published_none(self, db_session): release = DBReleaseFactory.create() diff --git a/tests/unit/test_csp.py b/tests/unit/test_csp.py index 157a7733ab4a..77e186745d7c 100644 --- a/tests/unit/test_csp.py +++ b/tests/unit/test_csp.py @@ -249,6 +249,7 @@ def test_includeme(): "'self'", "https://api.github.com/repos/", "https://api.github.com/search/issues", + "https://gitlab.com/api/", "https://*.google-analytics.com", "https://*.analytics.google.com", "https://*.googletagmanager.com", diff --git a/warehouse/csp.py b/warehouse/csp.py index 5ee9cf303230..1099279f50b6 100644 --- a/warehouse/csp.py +++ b/warehouse/csp.py @@ -97,6 +97,7 @@ def _connect_src_settings(config) -> list: SELF, "https://api.github.com/repos/", "https://api.github.com/search/issues", + "https://gitlab.com/api/", "https://*.google-analytics.com", "https://*.analytics.google.com", "https://*.googletagmanager.com", diff --git a/warehouse/locale/messages.pot b/warehouse/locale/messages.pot index 29f7ffbae55e..bd16af04cc32 100644 --- a/warehouse/locale/messages.pot +++ b/warehouse/locale/messages.pot @@ -840,8 +840,8 @@ msgstr "" #: warehouse/templates/manage/project/release.html:200 #: warehouse/templates/manage/project/releases.html:140 #: warehouse/templates/manage/project/releases.html:179 -#: warehouse/templates/packaging/detail.html:398 -#: warehouse/templates/packaging/detail.html:417 +#: warehouse/templates/packaging/detail.html:405 +#: warehouse/templates/packaging/detail.html:424 #: warehouse/templates/pages/classifiers.html:25 #: warehouse/templates/pages/help.html:20 #: warehouse/templates/pages/help.html:228 @@ -2706,7 +2706,7 @@ msgstr "" #: warehouse/templates/manage/account/recovery_codes-provision.html:58 #: warehouse/templates/manage/account/totp-provision.html:57 #: warehouse/templates/manage/unverified-account.html:179 -#: warehouse/templates/packaging/detail.html:158 +#: warehouse/templates/packaging/detail.html:165 #: warehouse/templates/pages/classifiers.html:38 msgid "Copy to clipboard" msgstr "" @@ -2877,7 +2877,7 @@ msgid "These details have been verified by PyPI" msgstr "" #: warehouse/templates/includes/packaging/project-data.html:23 -#: warehouse/templates/includes/packaging/project-data.html:137 +#: warehouse/templates/includes/packaging/project-data.html:184 msgid "Project links" msgstr "" @@ -2886,18 +2886,22 @@ msgid "GitHub Statistics" msgstr "" #: warehouse/templates/includes/packaging/project-data.html:56 +#: warehouse/templates/includes/packaging/project-data.html:102 msgid "Repository" msgstr "" #: warehouse/templates/includes/packaging/project-data.html:63 +#: warehouse/templates/includes/packaging/project-data.html:109 msgid "Stars:" msgstr "" #: warehouse/templates/includes/packaging/project-data.html:71 +#: warehouse/templates/includes/packaging/project-data.html:117 msgid "Forks:" msgstr "" #: warehouse/templates/includes/packaging/project-data.html:79 +#: warehouse/templates/includes/packaging/project-data.html:125 msgid "Open issues:" msgstr "" @@ -2905,57 +2909,65 @@ msgstr "" msgid "Open PRs:" msgstr "" -#: warehouse/templates/includes/packaging/project-data.html:95 +#: warehouse/templates/includes/packaging/project-data.html:96 +msgid "GitLab Statistics" +msgstr "" + +#: warehouse/templates/includes/packaging/project-data.html:133 +msgid "Open merge requests:" +msgstr "" + +#: warehouse/templates/includes/packaging/project-data.html:142 msgid "Maintainers" msgstr "" -#: warehouse/templates/includes/packaging/project-data.html:97 +#: warehouse/templates/includes/packaging/project-data.html:144 msgid "Avatar for {username} from gravatar.com" msgstr "" -#: warehouse/templates/includes/packaging/project-data.html:112 -#: warehouse/templates/includes/packaging/project-data.html:153 +#: warehouse/templates/includes/packaging/project-data.html:159 +#: warehouse/templates/includes/packaging/project-data.html:200 msgid "Meta" msgstr "" -#: warehouse/templates/includes/packaging/project-data.html:117 -#: warehouse/templates/includes/packaging/project-data.html:165 -#: warehouse/templates/includes/packaging/project-data.html:171 +#: warehouse/templates/includes/packaging/project-data.html:164 +#: warehouse/templates/includes/packaging/project-data.html:212 +#: warehouse/templates/includes/packaging/project-data.html:218 msgid "Author:" msgstr "" -#: warehouse/templates/includes/packaging/project-data.html:124 -#: warehouse/templates/includes/packaging/project-data.html:178 -#: warehouse/templates/includes/packaging/project-data.html:184 +#: warehouse/templates/includes/packaging/project-data.html:171 +#: warehouse/templates/includes/packaging/project-data.html:225 +#: warehouse/templates/includes/packaging/project-data.html:231 #: warehouse/templates/pages/help.html:620 msgid "Maintainer:" msgstr "" -#: warehouse/templates/includes/packaging/project-data.html:134 +#: warehouse/templates/includes/packaging/project-data.html:181 msgid "Unverified details" msgstr "" -#: warehouse/templates/includes/packaging/project-data.html:135 +#: warehouse/templates/includes/packaging/project-data.html:182 msgid "These details have not been verified by PyPI" msgstr "" -#: warehouse/templates/includes/packaging/project-data.html:158 +#: warehouse/templates/includes/packaging/project-data.html:205 msgid "License:" msgstr "" -#: warehouse/templates/includes/packaging/project-data.html:192 +#: warehouse/templates/includes/packaging/project-data.html:239 msgid "Tags" msgstr "" -#: warehouse/templates/includes/packaging/project-data.html:204 +#: warehouse/templates/includes/packaging/project-data.html:251 msgid "Requires:" msgstr "" -#: warehouse/templates/includes/packaging/project-data.html:211 +#: warehouse/templates/includes/packaging/project-data.html:258 msgid "Provides-Extra:" msgstr "" -#: warehouse/templates/includes/packaging/project-data.html:221 +#: warehouse/templates/includes/packaging/project-data.html:268 #: warehouse/templates/pages/classifiers.html:16 #: warehouse/templates/pages/classifiers.html:21 #: warehouse/templates/pages/sitemap.html:39 @@ -5927,12 +5939,12 @@ msgid "Back to projects" msgstr "" #: warehouse/templates/manage/project/manage_project_base.html:70 -#: warehouse/templates/packaging/detail.html:293 +#: warehouse/templates/packaging/detail.html:300 msgid "This project has been quarantined." msgstr "" #: warehouse/templates/manage/project/manage_project_base.html:72 -#: warehouse/templates/packaging/detail.html:295 +#: warehouse/templates/packaging/detail.html:302 msgid "" "PyPI Admins need to review this project before it can be restored. While " "in quarantine, the project is not installable by clients, and cannot be " @@ -5940,7 +5952,7 @@ msgid "" msgstr "" #: warehouse/templates/manage/project/manage_project_base.html:79 -#: warehouse/templates/packaging/detail.html:302 +#: warehouse/templates/packaging/detail.html:309 #, python-format msgid "" "Read more in the project in quarantine help " @@ -6810,123 +6822,123 @@ msgstr "" msgid "RSS: latest releases for %(project_name)s" msgstr "" -#: warehouse/templates/packaging/detail.html:160 +#: warehouse/templates/packaging/detail.html:167 msgid "Copy PIP instructions" msgstr "" -#: warehouse/templates/packaging/detail.html:171 +#: warehouse/templates/packaging/detail.html:178 msgid "This project has been quarantined" msgstr "" -#: warehouse/templates/packaging/detail.html:177 +#: warehouse/templates/packaging/detail.html:184 msgid "This release has been yanked" msgstr "" -#: warehouse/templates/packaging/detail.html:183 +#: warehouse/templates/packaging/detail.html:190 #, python-format msgid "Stable version available (%(version)s)" msgstr "" -#: warehouse/templates/packaging/detail.html:187 +#: warehouse/templates/packaging/detail.html:194 #, python-format msgid "Newer version available (%(version)s)" msgstr "" -#: warehouse/templates/packaging/detail.html:191 +#: warehouse/templates/packaging/detail.html:198 msgid "Latest version" msgstr "" -#: warehouse/templates/packaging/detail.html:196 +#: warehouse/templates/packaging/detail.html:203 #, python-format msgid "Released: %(release_date)s" msgstr "" -#: warehouse/templates/packaging/detail.html:210 +#: warehouse/templates/packaging/detail.html:217 msgid "No project description provided" msgstr "" -#: warehouse/templates/packaging/detail.html:223 +#: warehouse/templates/packaging/detail.html:230 msgid "Navigation" msgstr "" -#: warehouse/templates/packaging/detail.html:224 -#: warehouse/templates/packaging/detail.html:258 +#: warehouse/templates/packaging/detail.html:231 +#: warehouse/templates/packaging/detail.html:265 #, python-format msgid "Navigation for %(project)s" msgstr "" -#: warehouse/templates/packaging/detail.html:227 -#: warehouse/templates/packaging/detail.html:261 +#: warehouse/templates/packaging/detail.html:234 +#: warehouse/templates/packaging/detail.html:268 msgid "Project description. Focus will be moved to the description." msgstr "" -#: warehouse/templates/packaging/detail.html:229 -#: warehouse/templates/packaging/detail.html:263 -#: warehouse/templates/packaging/detail.html:313 +#: warehouse/templates/packaging/detail.html:236 +#: warehouse/templates/packaging/detail.html:270 +#: warehouse/templates/packaging/detail.html:320 msgid "Project description" msgstr "" -#: warehouse/templates/packaging/detail.html:233 -#: warehouse/templates/packaging/detail.html:273 +#: warehouse/templates/packaging/detail.html:240 +#: warehouse/templates/packaging/detail.html:280 msgid "Release history. Focus will be moved to the history panel." msgstr "" -#: warehouse/templates/packaging/detail.html:235 -#: warehouse/templates/packaging/detail.html:275 -#: warehouse/templates/packaging/detail.html:335 +#: warehouse/templates/packaging/detail.html:242 +#: warehouse/templates/packaging/detail.html:282 +#: warehouse/templates/packaging/detail.html:342 msgid "Release history" msgstr "" -#: warehouse/templates/packaging/detail.html:240 -#: warehouse/templates/packaging/detail.html:280 +#: warehouse/templates/packaging/detail.html:247 +#: warehouse/templates/packaging/detail.html:287 msgid "Download files. Focus will be moved to the project files." msgstr "" -#: warehouse/templates/packaging/detail.html:242 -#: warehouse/templates/packaging/detail.html:282 -#: warehouse/templates/packaging/detail.html:397 +#: warehouse/templates/packaging/detail.html:249 +#: warehouse/templates/packaging/detail.html:289 +#: warehouse/templates/packaging/detail.html:404 msgid "Download files" msgstr "" -#: warehouse/templates/packaging/detail.html:267 +#: warehouse/templates/packaging/detail.html:274 msgid "Project details. Focus will be moved to the project details." msgstr "" -#: warehouse/templates/packaging/detail.html:269 -#: warehouse/templates/packaging/detail.html:327 +#: warehouse/templates/packaging/detail.html:276 +#: warehouse/templates/packaging/detail.html:334 msgid "Project details" msgstr "" -#: warehouse/templates/packaging/detail.html:309 -#: warehouse/templates/packaging/detail.html:384 +#: warehouse/templates/packaging/detail.html:316 +#: warehouse/templates/packaging/detail.html:391 msgid "Reason this release was yanked:" msgstr "" -#: warehouse/templates/packaging/detail.html:320 +#: warehouse/templates/packaging/detail.html:327 msgid "The author of this package has not provided a project description" msgstr "" -#: warehouse/templates/packaging/detail.html:337 +#: warehouse/templates/packaging/detail.html:344 msgid "Release notifications" msgstr "" -#: warehouse/templates/packaging/detail.html:338 +#: warehouse/templates/packaging/detail.html:345 msgid "RSS feed" msgstr "" -#: warehouse/templates/packaging/detail.html:350 +#: warehouse/templates/packaging/detail.html:357 msgid "This version" msgstr "" -#: warehouse/templates/packaging/detail.html:370 +#: warehouse/templates/packaging/detail.html:377 msgid "pre-release" msgstr "" -#: warehouse/templates/packaging/detail.html:375 +#: warehouse/templates/packaging/detail.html:382 msgid "yanked" msgstr "" -#: warehouse/templates/packaging/detail.html:398 +#: warehouse/templates/packaging/detail.html:405 #, python-format msgid "" "Download the file for your platform. If you're not sure which to choose, " @@ -6934,24 +6946,24 @@ msgid "" "target=\"_blank\" rel=\"noopener\">installing packages." msgstr "" -#: warehouse/templates/packaging/detail.html:400 +#: warehouse/templates/packaging/detail.html:407 msgid "Source Distribution" msgid_plural "Source Distributions" msgstr[0] "" msgstr[1] "" -#: warehouse/templates/packaging/detail.html:416 +#: warehouse/templates/packaging/detail.html:423 msgid "No source distribution files available for this release." msgstr "" -#: warehouse/templates/packaging/detail.html:417 +#: warehouse/templates/packaging/detail.html:424 #, python-format msgid "" "See tutorial on generating distribution archives." msgstr "" -#: warehouse/templates/packaging/detail.html:424 +#: warehouse/templates/packaging/detail.html:431 msgid "Built Distribution" msgid_plural "Built Distributions" msgstr[0] "" diff --git a/warehouse/packaging/models.py b/warehouse/packaging/models.py index 3f6f397d8ae9..949635f6e7c9 100644 --- a/warehouse/packaging/models.py +++ b/warehouse/packaging/models.py @@ -736,38 +736,57 @@ def urls_by_verify_status(self, *, verified: bool): _urls[name] = url return _urls - @property - def verified_user_name_and_repo_name(self): + def verified_user_name_and_repo_name( + self, domains: set[str], reserved_names: typing.Sequence[str] | None = None + ): for _, url in self.urls_by_verify_status(verified=True).items(): try: parsed = parse_url(url) except LocationParseError: continue segments = parsed.path.strip("/").split("/") if parsed.path else [] - if parsed.netloc in {"github.com", "www.github.com"} and len(segments) >= 2: + if parsed.netloc in domains and len(segments) >= 2: user_name, repo_name = segments[:2] - if user_name in GITHUB_RESERVED_NAMES: + if reserved_names and user_name in reserved_names: continue if repo_name.endswith(".git"): repo_name = repo_name.removesuffix(".git") return user_name, repo_name return None, None + @property + def verified_github_user_name_and_repo_name(self): + return self.verified_user_name_and_repo_name( + {"github.com", "www.github.com"}, GITHUB_RESERVED_NAMES + ) + @property def verified_github_repo_info_url(self): - user_name, repo_name = self.verified_user_name_and_repo_name + user_name, repo_name = self.verified_github_user_name_and_repo_name if user_name and repo_name: return f"https://api.github.com/repos/{user_name}/{repo_name}" @property def verified_github_open_issue_info_url(self): - user_name, repo_name = self.verified_user_name_and_repo_name + user_name, repo_name = self.verified_github_user_name_and_repo_name if user_name and repo_name: return ( f"https://api.github.com/search/issues?q=repo:{user_name}/{repo_name}" "+type:issue+state:open&per_page=1" ) + @property + def verified_gitlab_user_name_and_repo_name(self): + return self.verified_user_name_and_repo_name({"gitlab.com", "www.gitlab.com"}) + + @property + def verified_gitlab_repository(self): + user_name, repo_name = self.verified_gitlab_user_name_and_repo_name + if user_name and repo_name: + return f"{user_name}/{repo_name}" + + return None + @property def has_meta(self): return any( diff --git a/warehouse/static/js/warehouse/controllers/gitlab_repo_info_controller.js b/warehouse/static/js/warehouse/controllers/gitlab_repo_info_controller.js new file mode 100644 index 000000000000..fe8164458100 --- /dev/null +++ b/warehouse/static/js/warehouse/controllers/gitlab_repo_info_controller.js @@ -0,0 +1,41 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Controller } from "@hotwired/stimulus"; + +export default class extends Controller { + static targets = [ + "forksCount", + "forksUrl", + "openIssuesCount", + "openIssuesUrl", + "starrersCount", + "starrersUrl", + "openMRsCount", + "openMRsUrl", + ]; + + updateStats(stats) { + this.forksCountTarget.textContent = stats.forks; + this.forksUrlTarget.href = stats.forks_url; + this.openIssuesCountTarget.textContent = stats.issues; + this.openIssuesUrlTarget.href = stats.issues_url; + this.openMRsCountTarget.textContent = stats.MRs; + this.openMRsUrlTarget.href = stats.MRs_url; + this.starrersCountTarget.textContent = stats.starrers; + this.starrersUrlTarget.href = stats.starrers_url; + + // unhide the container now that the data is populated + this.element.classList.remove("hidden"); + } +} diff --git a/warehouse/static/js/warehouse/controllers/gitlab_repo_stats_controller.js b/warehouse/static/js/warehouse/controllers/gitlab_repo_stats_controller.js new file mode 100644 index 000000000000..3baf9dba386d --- /dev/null +++ b/warehouse/static/js/warehouse/controllers/gitlab_repo_stats_controller.js @@ -0,0 +1,91 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Controller } from "@hotwired/stimulus"; + +export default class extends Controller { + static outlets = ["gitlab-repo-info"]; + static values = { + repository: String, + }; + + connect() { + this.load(); + } + + load() { + const get_project_objects_count = (project_api_url, project_object, fetchParams) => { + return fetch( + `${project_api_url}/${project_object}?` + new URLSearchParams({ + "state": "opened", + "per_page": 1, + }), + fetchParams + ).then((response) => { + if(response.ok) { + return response.json().then( + (data) => Object.keys(data).length ? response.headers.get("X-Total-Pages") : 0 + ); + } + + console.error( + `Received ${response.status} HTTP code while fetching Gitlab ${project_object} data. The response is "${response.text}"` + ); + + return 0; + }).catch(error => { + console.error(`An error ocured while fetching Gitlab ${project_object} data: ${error.message || error}`); + return 0; + }); + }; + + const GITLAB_API_URL = "https://gitlab.com/api/v4"; + const project_id = encodeURIComponent(this.repositoryValue); + const fetchParams = { + method: "GET", + mode: "cors", + }; + + const project_api_url = `${GITLAB_API_URL}/projects/${project_id}`; + const project_info = fetch( + project_api_url, + fetchParams + ).then((response) => + response.ok === true ? response.json() : null + ); + + const issues = get_project_objects_count(project_api_url, "issues", fetchParams); + const merge_requests = get_project_objects_count(project_api_url, "merge_requests", fetchParams); + + Promise.all([project_info, issues, merge_requests]).then( + ([project, issues_count, merge_requests_count]) => { + const stats = { + issues: issues_count, + issues_url: project.web_url + "/-/issues", + starrers: project.star_count || 0, + starrers_url: project.web_url + "/-/starrers", + forks: project.forks_count || 0, + forks_url: project.web_url + "/-/forks", + MRs: merge_requests_count, + MRs_url: project.web_url + "/-/merge_requests", + }; + this.gitlabRepoInfoOutlets.forEach((outlet) => + outlet.updateStats(stats) + ); + }) + // swallow errors, we don't want to show them to the user + .catch((error) => { + console.error(`An error ocured while fetching Gitlab data: ${error.message || error}`); + }); + } +} diff --git a/warehouse/templates/includes/packaging/project-data.html b/warehouse/templates/includes/packaging/project-data.html index 517b5a177adc..07ca3013072c 100644 --- a/warehouse/templates/includes/packaging/project-data.html +++ b/warehouse/templates/includes/packaging/project-data.html @@ -51,7 +51,7 @@
{% trans %}GitHub Statistics{% endtrans %}
{% endif %} + + {% if release.verified_gitlab_repository %} +
{% trans %}GitLab Statistics{% endtrans %}
+ + {% endif %} + {% if maintainers %}
{% trans %}Maintainers{% endtrans %}
{% for maintainer in maintainers %} diff --git a/warehouse/templates/packaging/detail.html b/warehouse/templates/packaging/detail.html index 725cb61774fe..0346c98b38fc 100644 --- a/warehouse/templates/packaging/detail.html +++ b/warehouse/templates/packaging/detail.html @@ -143,6 +143,13 @@

data-github-repo-stats-issue-url-value="{{release.verified_github_open_issue_info_url}}"> {% endif %} +{% if release.verified_gitlab_repository %} + +{% endif %}