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 logger = getLogger(__name__)

34

35

36 class Github(Source):

37 """Github source."""

38

39 def __init__(

40 self,

41 *,

42 log_prefix: str = "",

43 full_name: str,

44 package_name: str | None = None,

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

46 semver: str | None = None,

47 latest_release: bool | None = None,

48 tarball: bool | None = None,

49 zipball: bool | None = None,

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

51 ) -> None:

52 """Initialize a Github source.

53

54 Args:

55 log_prefix: Log prefix.

56 full_name: Repository full name. Mandatory.

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

58 tags: List of tags.

59 semver: Semantic version applied on tags.

60 latest_release: Get latest release.

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

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

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

64 """

65 super().__init__()

66 self._log_prefix = log_prefix

67 self._full_name = full_name

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

69 self._tags = tags or []

70 self._semver = semver

71 self._latest_release = latest_release or False

72 self._tarball = tarball or False

73 self._zipball = zipball or False

74 self._assets = assets or []

75

76 self._repo = PyGithub(

77 login_or_token=getenv("GITHUB_TOKEN"),

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

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

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

81

82 def __str__(self) -> str:

83 """Return string representation.

84

85 Returns:

86 A string.

87 """

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

89

90 @property

91 def package_files(self) -> list[PackageFile]:

92 """Return list of package files.

93

94 Returns:

95 List of package files.

96 """

97 package_files: list[PackageFile] = []

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

99 self._append_package_file_from_release(package_files, release)

100 return package_files

101

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

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

104 matches = Matcher(

105 self._tags,

106 self._semver,

107 log_prefix=self._log_prefix,

108 ).match(

109 self._get_all_tag_names,

110 )

111 self._matching_releases = {}

112 for match in matches:

113 self._append_matching_release(match[0])

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

115 try:

116 latest_release = self._repo.get_latest_release()

117 self._matching_releases[latest_release.tag_name] = latest_release

118 except UnknownObjectException as err2:

119 logger.warning(

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

121 self._log_prefix,

122 self._full_name,

123 str(err2),

124 )

125 return self._matching_releases

126

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

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

129

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

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

132 self._all_releases = {}

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

134 self._all_releases[release.tag_name] = release

135 return self._all_releases

136

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

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

139 try:

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

141 except UnknownObjectException as err:

142 logger.warning(

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

144 self._log_prefix,

145 self._full_name,

146 tag_name,

147 str(err),

148 )

149 else:

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

151

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

153 if self._tarball:

154 package_files.append(

155 PackageFile(

156 release.tarball_url,

157 "raw",

158 self._package_name,

159 release.tag_name,

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

161 )

162 )

163 if self._zipball:

164 package_files.append(

165 PackageFile(

166 release.zipball_url,

167 "raw",

168 self._package_name,

169 release.tag_name,

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

171 )

172 )

173 if self._assets:

174 try:

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

176 assets = release.assets

177 except AttributeError:

178 assets = list(release.get_assets())

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

180 for asset_name in self._assets:

181 try:

182 package_files.append(

183 PackageFile(

184 assets_map[asset_name],

185 "raw",

186 self._package_name,

187 release.tag_name,

188 asset_name,

189 )

190 )

191 except KeyError:

192 logger.warning(

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

194 self._log_prefix,

195 asset_name,

196 self._full_name,

197 release.tag_name,

198 )