commit 96588c50d99d2d0d472fbef27cec27291e5871a5 from: Daniele Trifirò via: Jelmer Vernooij date: Sun Oct 23 23:27:07 2022 UTC add support for git credential helpers - add misc url-matching functions Extracted from https://github.com/jelmer/dulwich/pull/976 commit - 1997f839a022252a607d6dff033bdd52f62fff41 commit + 96588c50d99d2d0d472fbef27cec27291e5871a5 blob - /dev/null blob + 683bb6c0b04d41602f0de1198baf22f44d14e62d (mode 644) --- /dev/null +++ dulwich/credentials.py @@ -0,0 +1,89 @@ +# credentials.py -- support for git credential helpers + +# Copyright (C) 2022 Daniele Trifirò +# +# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU +# General Public License as public by the Free Software Foundation; version 2.0 +# or (at your option) any later version. You can redistribute it and/or +# modify it under the terms of either of these two licenses. +# +# 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. +# +# You should have received a copy of the licenses; if not, see +# for a copy of the GNU General Public License +# and for a copy of the Apache +# License, Version 2.0. +# + +"""Support for git credential helpers + +https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage + +""" +import sys +from typing import Iterator, Optional +from urllib.parse import ParseResult, urlparse + +from dulwich.config import ConfigDict, SectionLike + + +def match_urls(url: ParseResult, url_prefix: ParseResult) -> bool: + base_match = ( + url.scheme == url_prefix.scheme + and url.hostname == url_prefix.hostname + and url.port == url_prefix.port + ) + user_match = url.username == url_prefix.username if url_prefix.username else True + path_match = url.path.rstrip("/").startswith(url_prefix.path.rstrip()) + return base_match and user_match and path_match + + +def match_partial_url(valid_url: ParseResult, partial_url: str) -> bool: + """matches a parsed url with a partial url (no scheme/netloc)""" + if "://" not in partial_url: + parsed = urlparse("scheme://" + partial_url) + else: + parsed = urlparse(partial_url) + if valid_url.scheme != parsed.scheme: + return False + + if any( + ( + (parsed.hostname and valid_url.hostname != parsed.hostname), + (parsed.username and valid_url.username != parsed.username), + (parsed.port and valid_url.port != parsed.port), + (parsed.path and parsed.path.rstrip("/") != valid_url.path.rstrip("/")), + ), + ): + return False + + return True + + +def urlmatch_credential_sections( + config: ConfigDict, url: Optional[str] +) -> Iterator[SectionLike]: + """Returns credential sections from the config which match the given URL""" + encoding = config.encoding or sys.getdefaultencoding() + parsed_url = urlparse(url or "") + for config_section in config.sections(): + if config_section[0] != b"credential": + continue + + if len(config_section) < 2: + yield config_section + continue + + config_url = config_section[1].decode(encoding) + parsed_config_url = urlparse(config_url) + if parsed_config_url.scheme and parsed_config_url.netloc: + is_match = match_urls(parsed_url, parsed_config_url) + else: + is_match = match_partial_url(parsed_url, config_url) + + if is_match: + yield config_section blob - c1e093590e4f93b4ec4b8c329f5c256f9c6db414 blob + 17af13b156915c56314a8878ed9284af28f9356a --- dulwich/tests/__init__.py +++ dulwich/tests/__init__.py @@ -117,6 +117,7 @@ def self_test_suite(): "bundle", "client", "config", + "credentials", "diff_tree", "fastexport", "file", blob - /dev/null blob + 7bb90e56df3e207bf7337bb4133fc9c5506c9b48 (mode 644) --- /dev/null +++ dulwich/tests/test_credentials.py @@ -0,0 +1,75 @@ +# test_credentials.py -- tests for credentials.py + +# Copyright (C) 2022 Daniele Trifirò +# +# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU +# General Public License as public by the Free Software Foundation; version 2.0 +# or (at your option) any later version. You can redistribute it and/or +# modify it under the terms of either of these two licenses. +# +# 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. +# +# You should have received a copy of the licenses; if not, see +# for a copy of the GNU General Public License +# and for a copy of the Apache +# License, Version 2.0. +# + +from urllib.parse import urlparse + +from dulwich.config import ConfigDict +from dulwich.credentials import ( + match_partial_url, + match_urls, + urlmatch_credential_sections, +) +from dulwich.tests import TestCase + + +class TestCredentialHelpersUtils(TestCase): + + def test_match_urls(self): + url = urlparse("https://github.com/jelmer/dulwich/") + url_1 = urlparse("https://github.com/jelmer/dulwich") + url_2 = urlparse("https://github.com/jelmer") + url_3 = urlparse("https://github.com") + self.assertTrue(match_urls(url, url_1)) + self.assertTrue(match_urls(url, url_2)) + self.assertTrue(match_urls(url, url_3)) + + non_matching = urlparse("https://git.sr.ht/") + self.assertFalse(match_urls(url, non_matching)) + + def test_match_partial_url(self): + url = urlparse("https://github.com/jelmer/dulwich/") + self.assertTrue(match_partial_url(url, "github.com")) + self.assertFalse(match_partial_url(url, "github.com/jelmer/")) + self.assertTrue(match_partial_url(url, "github.com/jelmer/dulwich")) + self.assertFalse(match_partial_url(url, "github.com/jel")) + self.assertFalse(match_partial_url(url, "github.com/jel/")) + + def test_urlmatch_credential_sections(self): + config = ConfigDict() + config.set((b"credential", "https://github.com"), b"helper", "foo") + config.set((b"credential", "git.sr.ht"), b"helper", "foo") + config.set(b"credential", b"helper", "bar") + + self.assertEqual( + list(urlmatch_credential_sections(config, "https://github.com")), [ + (b"credential", b"https://github.com"), + (b"credential",), + ]) + + self.assertEqual( + list(urlmatch_credential_sections(config, "https://git.sr.ht")), [ + (b"credential", b"git.sr.ht"), + (b"credential",), + ]) + + self.assertEqual( + list(urlmatch_credential_sections(config, "missing_url")), [ + (b"credential",)])