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

2#

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

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

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

6# (at your option) any later version.

7#

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

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

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

11# GNU Lesser General Public License for more details.

12#

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

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

15

16 import logging

17 from copy import deepcopy

18

19 from gitlabracadabra.objects.object import GitLabracadabraObject

20

21 logger = logging.getLogger(__name__)

22

23

24 class BoardsMixin(GitLabracadabraObject):

25 """Object with boards."""

26

27 """_process_boards()

28

29 Process the boards param.

30 """

31

32 def _process_boards(self, param_name, param_value, *, dry_run=False, skip_save=False):

33 assert param_name == "boards" # noqa: S101

34 assert not skip_save # noqa: S101

35

36 unknown_boards = self._content.get("unknown_boards", "warn")

37 unknown_board_lists = self._content.get("unknown_board_lists", "delete")

38

39 current_boards = dict(

40 [[current_board.name, current_board] for current_board in self._obj.boards.list(all=True)]

41 )

42 target_boards = dict([[target_board["name"], deepcopy(target_board)] for target_board in param_value])

43 target_boards_by_old_name = dict(

44 [

45 [target_board["old_name"], deepcopy(target_board)]

46 for target_board in param_value

47 if "old_name" in target_board

48 ]

49 )

50

51 # We do not reuse labels from LabelsMixin, to have fresh data

52 try:

53 self._current_labels = dict(

54 [[current_label.name, current_label] for current_label in self._obj.labels.list(all=True)]

55 )

56 except AttributeError:

57 # https://github.com/python-gitlab/python-gitlab/pull/847

58 logger.error(

59 "[%s] Unable to manage labels: %s", self._name, "group labels requires python-gitlab >= 1.11.0"

60 )

61 return

62

63 # We first check for already existing boards

64 for current_board_name, current_board in sorted(current_boards.items()):

65 if current_board_name in target_boards:

66 target_board = target_boards.pop(current_board_name)

67 elif current_board_name in target_boards_by_old_name:

68 target_board = target_boards_by_old_name[current_board_name]

69 target_boards.pop(target_board["name"])

70 if dry_run: 70 ↛ 71 line 70 didn't jump to line 71 because the condition on line 70 was never true

71 logger.info(

72 "[%s] NOT Changing board %s %s: %s -> %s (dry-run)",

73 self._name,

74 current_board_name,

75 "name",

76 current_board_name,

77 target_board["name"],

78 )

79 else:

80 logger.info(

81 "[%s] Changing board %s %s: %s -> %s",

82 self._name,

83 current_board_name,

84 "name",

85 current_board_name,

86 target_board["name"],

87 )

88 current_board.name = target_board["name"]

89 try:

90 current_board.save()

91 except KeyError:

92 # v1.10.0 - feat: add support for board update

93 # https://github.com/python-gitlab/python-gitlab/pull/804

94 logger.error(

95 "[%s] Unable to update board: %s",

96 self._name,

97 "board update requires python-gitlab >= 1.10.0",

98 )

99 return

100 else:

101 target_board = None

102 if target_board:

103 for target_board_param_name, target_board_param_value in target_board.items():

104 if target_board_param_name == "lists":

105 unknown_lists = target_board.get("unknown_lists", unknown_board_lists)

106 self._handle_board_lists(

107 current_board, target_board_param_value, unknown_lists, dry_run=dry_run

108 )

109 continue

110 if target_board_param_name in ["old_name", "unknown_lists"]:

111 continue

112 try:

113 current_board_param_value = getattr(current_board, target_board_param_name)

114 except AttributeError:

115 logger.info(

116 "[%s] NOT Changing boards %s %s: %s -> %s " "(current value is not available)",

117 self._name,

118 current_board_name,

119 target_board_param_name,

120 None,

121 target_board_param_value,

122 )

123 continue

124 if current_board_param_value != target_board_param_value:

125 if dry_run: 125 ↛ 126 line 125 didn't jump to line 126 because the condition on line 125 was never true

126 logger.info(

127 "[%s] NOT Changing board %s %s: %s -> %s (dry-run)",

128 self._name,

129 current_board_name,

130 target_board_param_name,

131 current_board_param_value,

132 target_board_param_value,

133 )

134 else:

135 logger.info(

136 "[%s] Changing board %s %s: %s -> %s",

137 self._name,

138 current_board_name,

139 target_board_param_name,

140 current_board_param_value,

141 target_board_param_value,

142 )

143 setattr(current_board, target_board_param_name, target_board_param_value)

144 try:

145 current_board.save()

146 except KeyError:

147 # v1.10.0 - feat: add support for board update

148 # https://github.com/python-gitlab/python-gitlab/pull/804

149 logger.error(

150 "[%s] Unable to update board: %s",

151 self._name,

152 "board update requires python-gitlab >= 1.10.0",

153 )

154 return

155 elif unknown_boards in ["delete", "remove"]: 155 ↛ 161 line 155 didn't jump to line 161 because the condition on line 155 was always true

156 if dry_run: 156 ↛ 157 line 156 didn't jump to line 157 because the condition on line 156 was never true

157 logger.info("[%s] NOT Removing board %s (dry-run)", self._name, current_board_name)

158 else:

159 logger.info("[%s] Removing board %s", self._name, current_board_name)

160 current_board.delete()

161 elif unknown_boards not in ["ignore", "skip"]:

162 logger.warning("[%s] NOT Removing board: %s", self._name, current_board_name)

163 # Remaining boards

164 for target_board_name, target_board in sorted(target_boards.items()):

165 target_board_lists = target_board.pop("lists", [])

166 unknown_lists = target_board.pop("unknown_lists", unknown_board_lists)

167 if dry_run: 167 ↛ 168 line 167 didn't jump to line 168 because the condition on line 167 was never true

168 logger.info(

169 "[%s] NOT Adding board %s: %s -> %s (dry-run)", self._name, target_board_name, None, target_board

170 )

171 else:

172 logger.info("[%s] Adding board %s: %s -> %s", self._name, target_board_name, None, target_board)

173 board = self._obj.boards.create(target_board)

174 self._handle_board_lists(board, target_board_lists, unknown_lists, dry_run=dry_run)

175

176 """_handle_board_lists()

177

178 Handle board lists.

179 """

180

181 def _handle_board_lists(self, board, target_lists, unknown_lists, *, dry_run=False):

182 if len(target_lists) == 0 and unknown_lists in ["ignore", "skip"]: 182 ↛ 183 line 182 didn't jump to line 183 because the condition on line 182 was never true

183 return

184 board = self._obj.boards.get(board.id)

185 current_lists = dict([[current_list.label["name"], current_list] for current_list in board.lists.list()])

186 # We first add any missing list

187 for target_list_position, target_list in enumerate(target_lists):

188 if target_list["label"] in current_lists:

189 current_list = current_lists[target_list["label"]]

190 if target_list_position != current_list.position:

191 old_list_position = current_list.position

192 # Boards::Lists::MoveService

193 # https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/services/boards/lists/move_service.rb#L29

194 if old_list_position < target_list_position: 194 ↛ 195 line 194 didn't jump to line 195 because the condition on line 194 was never true

195 self._decrement_intermediate_lists(current_lists, old_list_position, target_list_position)

196 else:

197 self._increment_intermediate_lists(current_lists, old_list_position, target_list_position)

198 current_list.position = target_list_position

199 if dry_run: 199 ↛ 200 line 199 didn't jump to line 200 because the condition on line 199 was never true

200 logger.info(

201 "[%s] NOT Changing board list position %s: %s -> %s (dry-run)",

202 self._name,

203 target_list,

204 current_list.position,

205 target_list_position,

206 )

207 else:

208 logger.info(

209 "[%s] Changing board list position %s: %s -> %s (dry-run)",

210 self._name,

211 target_list,

212 current_list.position,

213 target_list_position,

214 )

215 current_list.save()

216 else:

217 if target_list["label"] not in self._current_labels:

218 logger.warning(

219 "[%s] Label not found for board %s: %s", self._name, board.name, target_list["label"]

220 )

221 continue

222 label_id = self._current_labels[target_list["label"]].id

223 if dry_run: 223 ↛ 224 line 223 didn't jump to line 224 because the condition on line 223 was never true

224 logger.info("[%s] NOT Adding board list %s (dry-run)", self._name, target_list)

225 else:

226 logger.info("[%s] Adding board list %s", self._name, target_list)

227 board.lists.create({"label_id": label_id})

228

229 if unknown_lists in ["ignore", "skip"]: 229 ↛ 230 line 229 didn't jump to line 230 because the condition on line 229 was never true

230 return

231 # Delete any remaining lists

232 for current_list_label, current_list in sorted(current_lists.items()):

233 current_list_dict = {"label": current_list_label}

234 if current_list_dict in target_lists:

235 continue

236

237 if unknown_lists in ["delete", "remove"]: 237 ↛ 243 line 237 didn't jump to line 243 because the condition on line 237 was always true

238 if dry_run: 238 ↛ 239 line 238 didn't jump to line 239 because the condition on line 238 was never true

239 logger.info("[%s] NOT Removing board list %s (dry-run)", self._name, current_list_dict)

240 else:

241 logger.info("[%s] Removing board list %s", self._name, current_list_dict)

242 current_list.delete()

243 elif unknown_lists not in ["ignore", "skip"]:

244 logger.warning("[%s] NOT Removing board list: %s", self._name, current_list_dict)

245

246 """_decrement_intermediate_lists()

247

248 Decrement intermediate lists.

249 """

250

251 def _decrement_intermediate_lists(self, lists, old_position, new_position):

252 for list_object in lists.values():

253 if list_object.position > old_position and list_object.position <= new_position:

254 list_object.position = list_object.position - 1

255

256 """_increment_intermediate_lists()

257

258 Increment intermediate lists.

259 """

260

261 def _increment_intermediate_lists(self, lists, old_position, new_position):

262 for list_object in lists.values():

263 if list_object.position >= new_position and list_object.position < old_position:

264 list_object.position = list_object.position + 1