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 import logging

18 from copy import deepcopy

19 from re import match

20

21 from gitlab.exceptions import GitlabDeleteError

22

23 from gitlabracadabra.gitlab.access_levels import access_level_value

24 from gitlabracadabra.objects.object import GitLabracadabraObject

25

26 logger = logging.getLogger(__name__)

27

28

29 class MembersMixin(GitLabracadabraObject):

30 """Object with members."""

31

32 """_process_members()

33

34 Process the members param.

35 """

36

37 def _process_members(self, param_name, param_value, *, dry_run=False, skip_save=False):

38 assert param_name == "members" # noqa: S101

39 assert not skip_save # noqa: S101

40 param_value = deepcopy(param_value)

41 unknown_members = self._content.get("unknown_members", "warn")

42 current_members = dict([[member.username, member] for member in self._obj.members.list(all=True)])

43 # We first check for already existing members

44 for _member_username, member in sorted(current_members.items()):

45 self.connection.user_cache.map_user(member.id, member.username)

46 if member.username in param_value:

47 target_access_level = access_level_value(param_value[member.username])

48 if member.access_level != target_access_level: 48 ↛ 69 line 48 didn't jump to line 69 because the condition on line 48 was always true

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

50 logger.info(

51 "[%s] NOT Changing user %s (%s) access level: %s -> %s (dry-run)",

52 self._name,

53 member.username,

54 member.name,

55 member.access_level,

56 target_access_level,

57 )

58 else:

59 logger.info(

60 "[%s] Changing user %s (%s) access level: %s -> %s",

61 self._name,

62 member.username,

63 member.name,

64 member.access_level,

65 target_access_level,

66 )

67 member.access_level = target_access_level

68 member.save()

69 param_value.pop(member.username)

70 else:

71 if match(r"(project|group)_\d+_bot_[0-9a-f]+", member.username): 71 ↛ 73 line 71 didn't jump to line 73 because the condition on line 71 was never true

72 # bot

73 continue

74 if unknown_members == "warn": 74 ↛ 75 line 74 didn't jump to line 75 because the condition on line 74 was never true

75 logger.warning("[%s] NOT Removing member: %s (%s)", self._name, member.username, member.name)

76 if unknown_members not in ["delete", "remove"]:

77 # warn, ignore, skip

78 continue

79

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

81 logger.info("[%s] NOT Removing member %s (%s) (dry-run)", self._name, member.username, member.name)

82 continue

83 logger.info("[%s] Removing member %s (%s)", self._name, member.username, member.name)

84 try:

85 member.delete()

86 except GitlabDeleteError as e:

87 logger.warning(

88 "[%s] Unable to remove member %s (%s): %s",

89 self._name,

90 member.username,

91 member.name,

92 e.error_message,

93 )

94 # Remaining members

95 for target_username, target_user_access in sorted(param_value.items()):

96 user_id = self.connection.user_cache.id_from_username(target_username)

97 if user_id is None:

98 logger.warning("[%s] User not found %s", self._name, target_username)

99 continue

100 target_access_level = access_level_value(target_user_access)

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

102 logger.info(

103 "[%s] NOT Adding user %s: %s -> %s (dry-run)", self._name, target_username, 0, target_access_level

104 )

105 else:

106 logger.info("[%s] Adding user %s: %s -> %s", self._name, target_username, 0, target_access_level)

107 self._obj.members.create(

108 {

109 "user_id": user_id,

110 "access_level": target_access_level,

111 }

112 )