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 from __future__ import annotations

18

19 from logging import getLogger

20 from os import getenv

21 from typing import TYPE_CHECKING

22

23 from github import Github as PyGithub

24 from github.GithubException import UnknownObjectException

25

26 from gitlabracadabra.matchers import Matcher

27 from gitlabracadabra.packages.package_file import PackageFile

28 from gitlabracadabra.packages.source import Source

29

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

31 from github.GitRelease import GitRelease

32

33 from gitlabracadabra.packages.destination import Destination

34

35 logger = getLogger(__name__)

36

37

38 class Github(Source):

39 """Github source."""

40

41 def __init__(

42 self,

43 *,

44 log_prefix: str = "",

45 full_name: str,

46 package_name: str | None = None,

47 tags: list[str] | None = None,

48 semver: str | None = None,

49 latest_release: bool | None = None,

50 tarball: bool | None = None,

51 zipball: bool | None = None,

52 assets: list[str] | None = None,

53 ) -> None:

54 """Initialize a Github source.

55

56 Args:

57 log_prefix: Log prefix.

58 full_name: Repository full name. Mandatory.

59 package_name: Destination package name (defaults to repository name).

60 tags: List of tags.

61 semver: Semantic version applied on tags.

62 latest_release: Get latest release.

63 tarball: Get repository tarball (defaults to False).

64 zipball: Get repository zip (defaults to False).

65 assets: List of assets (None by default).

66 """

67 super().__init__()

68 self._log_prefix = log_prefix

69 self._full_name = full_name

70 self._package_name = package_name or full_name.split("/").pop()

71 self._tags = tags or []

72 self._semver = semver

73 self._latest_release = latest_release or False

74 self._tarball = tarball or False

75 self._zipball = zipball or False

76 self._assets = assets or []

77

78 self._repo = PyGithub(

79 login_or_token=getenv("GITHUB_TOKEN"),

80 ).get_repo(self._full_name, lazy=True)

81 self._all_releases: dict[str, GitRelease] | None = None

82 self._matching_releases: dict[str, GitRelease] | None = None

83

84 def __str__(self) -> str:

85 """Return string representation.

86

87 Returns:

88 A string.

89 """

90 return f"Github repository (full_name={self._full_name})"

91

92 def package_files(

93 self,

94 destination: Destination, # noqa: ARG002

95 ) -> list[PackageFile]:

96 """Return list of package files.

97

98 Returns:

99 List of package files.

100 """

101 package_files: list[PackageFile] = []

102 for release in self._get_matching_releases().values():

103 self._append_package_file_from_release(package_files, release)

104 return package_files

105

106 def _get_matching_releases(self) -> dict[str, GitRelease]:

107 if self._matching_releases is None: 107 ↛ 129 line 107 didn't jump to line 129 because the condition on line 107 was always true

108 matches = Matcher(

109 self._tags,

110 self._semver,

111 log_prefix=self._log_prefix,

112 ).match(

113 self._get_all_tag_names,

114 )

115 self._matching_releases = {}

116 for match in matches:

117 self._append_matching_release(match[0])

118 if self._latest_release: 118 ↛ 129 line 118 didn't jump to line 129 because the condition on line 118 was always true

119 try:

120 latest_release = self._repo.get_latest_release()

121 self._matching_releases[latest_release.tag_name] = latest_release

122 except UnknownObjectException as err2:

123 logger.warning(

124 "%sError getting package files from repository %s, latest release: %s",

125 self._log_prefix,

126 self._full_name,

127 str(err2),

128 )

129 return self._matching_releases

130

131 def _get_all_tag_names(self) -> list[str]:

132 return list(self._get_all_releases().keys())

133

134 def _get_all_releases(self) -> dict[str, GitRelease]:

135 if self._all_releases is None: 135 ↛ 139 line 135 didn't jump to line 139 because the condition on line 135 was always true

136 self._all_releases = {}

137 for release in self._repo.get_releases():

138 self._all_releases[release.tag_name] = release

139 return self._all_releases

140

141 def _append_matching_release(self, tag_name: str) -> None:

142 if self._all_releases is None: 142 ↛ 143 line 142 didn't jump to line 143 because the condition on line 142 was never true

143 try:

144 self._matching_releases[tag_name] = self._repo.get_release(tag_name) # type: ignore

145 except UnknownObjectException as err:

146 logger.warning(

147 "%sError getting package files from repository %s, release with tag %s: %s",

148 self._log_prefix,

149 self._full_name,

150 tag_name,

151 str(err),

152 )

153 else:

154 self._matching_releases[tag_name] = self._all_releases[tag_name] # type: ignore

155

156 def _append_package_file_from_release(self, package_files: list[PackageFile], release: GitRelease) -> None:

157 if self._tarball:

158 package_files.append(

159 PackageFile(

160 release.tarball_url,

161 "generic",

162 self._package_name,

163 release.tag_name,

164 "{}-{}.tar.gz".format(self._full_name.split("/").pop(), release.tag_name),

165 )

166 )

167 if self._zipball:

168 package_files.append(

169 PackageFile(

170 release.zipball_url,

171 "generic",

172 self._package_name,

173 release.tag_name,

174 "{}-{}.zip".format(self._full_name.split("/").pop(), release.tag_name),

175 )

176 )

177 if self._assets:

178 try:

179 # https://github.com/PyGithub/PyGithub/pull/1899

180 assets = release.assets

181 except AttributeError:

182 assets = list(release.get_assets())

183 assets_map = {asset.name: asset.browser_download_url for asset in assets}

184 for asset_name in self._assets:

185 try:

186 package_files.append(

187 PackageFile(

188 assets_map[asset_name],

189 "generic",

190 self._package_name,

191 release.tag_name,

192 asset_name,

193 )

194 )

195 except KeyError:

196 logger.warning(

197 '%sAsset "%s" not found from repository %s in release with tag %s',

198 self._log_prefix,

199 asset_name,

200 self._full_name,

201 release.tag_name,

202 )