1#

2# Copyright (C) 2019-2025 Mathieu Parent <math.parent@gmail.com>

3#

4# This program is free software: you can redistribute it and/or modify

5# it under the terms of the GNU Lesser General Public License as published by

6# the Free Software Foundation, either version 3 of the License, or

7# (at your option) any later version.

8#

9# This program is distributed in the hope that it will be useful,

10# but WITHOUT ANY WARRANTY; without even the implied warranty of

11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the

12# GNU Lesser General Public License for more details.

13#

14# You should have received a copy of the GNU Lesser General Public License

15# along with this program. If not, see <http://www.gnu.org/licenses/>.

16

17

18 from __future__ import annotations

19

20 from typing import TYPE_CHECKING

21 from urllib.parse import urlparse

22

23 from pygit2 import Passthrough, RemoteCallbacks, UserPass

24

25 from gitlabracadabra.gitlab.pygitlab import PyGitlab

26

27 if TYPE_CHECKING: 27 ↛ 28 line 27 didn't jump to line 28 because the condition on line 27 was never true

28 from pygit2.enums import CredentialType

29

30

31 class PyGit2(PyGitlab):

32 """PyGit2 wrapper."""

33

34 def pygit2_certificate_check(

35 self,

36 certificate: None, # noqa: ARG002

37 valid: bool, # noqa: ARG002,FBT001

38 host: str,

39 ) -> bool:

40 """Check certificate.

41

42 Args:

43 _certificate: Currently always None.

44 valid: Whether the TLS/SSH library thinks the certificate is valid.

45 host: The hostname we want to connect to.

46

47 Returns:

48 True to connect, False to abort.

49

50 Raises:

51 Passthrough: Use default behavior.

52 """

53 if self.pygitlab.ssl_verify is True:

54 raise Passthrough

55 if self.pygitlab.ssl_verify is False:

56 allowed_host = urlparse(self.api_url).hostname

57 if allowed_host is not None and host in {allowed_host, allowed_host.encode()}:

58 return True

59 raise Passthrough

60 # self.pygitlab.ssl_verify is a CA path, no way to verify

61 return False

62

63 def pygit2_credentials(

64 self,

65 url: str,

66 username_from_url: str | None, # noqa: ARG002

67 allowed_types: CredentialType, # noqa: ARG002

68 ) -> object:

69 """Get PyGit2 credentials.

70

71 Args:

72 url: The url of the remote.

73 _username_from_url: Username extracted from the url, if any.

74 _allowed_types: Combination of bitflags representing the credential types supported by the remote.

75

76 Returns:

77 A pygit2.UserPass.

78

79 Raises:

80 ValueError: No credentials found.

81 """

82 target_hostname = urlparse(url).hostname

83 allowed_hostname = urlparse(self.api_url).hostname

84 if allowed_hostname != target_hostname:

85 msg = f"Target hostname {target_hostname} not matching allowed hostname {allowed_hostname}"

86 raise ValueError(

87 msg,

88 )

89 if self.pygitlab.private_token:

90 return UserPass("oauth2", self.pygitlab.private_token)

91 if self.pygitlab.oauth_token:

92 return UserPass("oauth2", self.pygitlab.oauth_token)

93 if self.pygitlab.job_token:

94 return UserPass("gitlab-ci-token", self.pygitlab.job_token)

95 if self.pygitlab.http_username and self.pygitlab.http_password:

96 return UserPass(self.pygitlab.http_username, self.pygitlab.http_password)

97 msg = "No PyGit2 credentials for {target_hostname}"

98 raise ValueError(msg)

99

100 @property

101 def pygit2_remote_callbacks(self) -> RemoteCallbacks:

102 """Get PyGit2 RemoteCallbacks.

103

104 Returns:

105 A pygit2.RemoteCallbacks.

106 """

107 cb = RemoteCallbacks() # type: ignore[no-untyped-call]

108 cb.credentials = self.pygit2_credentials # type: ignore[method-assign]

109 if self.pygitlab.ssl_verify is not True: 109 ↛ 110 line 109 didn't jump to line 110 because the condition on line 109 was never true

110 cb.certificate_check = self.pygit2_certificate_check # type: ignore[method-assign]

111 return cb