1#!/usr/bin/env python

2#

3# Copyright (C) 2013-2017 Gauvain Pocentek <gauvain@pocentek.net>

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

5#

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

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

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

9# (at your option) any later version.

10#

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

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

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

14# GNU Lesser General Public License for more details.

15#

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

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

18

19 from __future__ import annotations

20

21 import logging

22 import sys

23 from argparse import ArgumentParser

24 from typing import TYPE_CHECKING

25

26 import gitlabracadabra

27 import gitlabracadabra.parser

28 from gitlabracadabra.gitlab.connections import GitlabConnections

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 collections.abc import Sequence

32

33 logger = logging.getLogger(__name__)

34

35

36 def _get_argument_parser() -> ArgumentParser:

37 parser = ArgumentParser(description="GitLabracadabra")

38 parser.add_argument("--version", help="Display the version.", action="store_true")

39 parser.add_argument("-v", "--verbose", "--fancy", help="Verbose mode", action="store_true")

40 parser.add_argument("-d", "--debug", help="Debug mode (display HTTP requests)", action="store_true")

41 parser.add_argument("--logging-format", help="Logging format", choices=["short", "long"], default="short")

42 parser.add_argument(

43 "-c", "--config-file", action="append", help=("Configuration file to use. Can be used " "multiple times.")

44 )

45 parser.add_argument(

46 "-g",

47 "--gitlab",

48 help=("Which configuration section should " "be used. If not defined, the default selection " "will be used."),

49 required=False,

50 )

51 parser.add_argument("--dry-run", help="Dry run", action="store_true")

52 parser.add_argument("--fail-on-errors", help="Fail on errors", action="store_true")

53 parser.add_argument("--fail-on-warnings", help="Fail on warnings", action="store_true")

54 parser.add_argument(

55 "--doc-markdown",

56 help=("Output the help for the given type (project, " "group, user, application_settings) as " "Markdown."),

57 )

58 parser.add_argument(

59 "action_files",

60 help="Action file. Can be used multiple times.",

61 metavar="ACTIONFILE.yml",

62 nargs="*",

63 default=["gitlabracadabra.yml"],

64 )

65

66 return parser

67

68

69 class ExitCodeHandler(logging.Handler):

70 def __init__(self) -> None:

71 logging.Handler.__init__(self)

72 self._max_levelno: int = logging.NOTSET

73

74 def emit(self, record: logging.LogRecord) -> None:

75 if record.levelno > self._max_levelno:

76 self._max_levelno = record.levelno

77

78 @property

79 def max_levelno(self) -> int:

80 return self._max_levelno

81

82

83 def main(args: Sequence[str] | None = None) -> None:

84 argument_parser = _get_argument_parser()

85

86 namespace = argument_parser.parse_args(args)

87

88 if namespace.version: 88 ↛ 89 line 88 didn't jump to line 89 because the condition on line 88 was never true

89 print(gitlabracadabra.__version__) # noqa: T201

90 sys.exit(0)

91

92 config_files = namespace.config_file

93 gitlab_id = namespace.gitlab

94

95 if namespace.logging_format == "long": 95 ↛ 96 line 95 didn't jump to line 96 because the condition on line 95 was never true

96 logging_format = "%(asctime)s [%(process)d] %(levelname)-8.8s %(name)s: %(message)s"

97 else:

98 logging_format = "%(levelname)-8.8s %(message)s"

99 log_level = logging.WARNING

100 if namespace.verbose: 100 ↛ 101 line 100 didn't jump to line 101 because the condition on line 100 was never true

101 log_level = logging.INFO

102 if namespace.debug: 102 ↛ 103 line 102 didn't jump to line 103 because the condition on line 102 was never true

103 log_level = logging.DEBUG

104 exit_code_handler = ExitCodeHandler()

105 logging.basicConfig(

106 format=logging_format,

107 level=log_level,

108 )

109 logging.root.addHandler(exit_code_handler)

110

111 if namespace.doc_markdown: 111 ↛ 112 line 111 didn't jump to line 112 because the condition on line 111 was never true

112 cls = gitlabracadabra.parser.GitlabracadabraParser.get_class_for(namespace.doc_markdown)

113 print(cls.doc_markdown()) # noqa: T201

114 sys.exit(0)

115

116 try:

117 GitlabConnections().load(gitlab_id, config_files, debug=namespace.debug)

118 except Exception as e: # noqa: BLE001

119 logger.error(str(e))

120 sys.exit(1)

121

122 # First pass: Load data and preflight checks

123 objects = {}

124 has_errors = False

125 for action_file in namespace.action_files:

126 if action_file.endswith((".yml", ".yaml")): 126 ↛ 129 line 126 didn't jump to line 129 because the condition on line 126 was always true

127 parser = gitlabracadabra.parser.GitlabracadabraParser.from_yaml_file(action_file)

128 else:

129 logger.error("Unhandled file: %s", action_file)

130 has_errors = True

131 continue

132 logger.debug("Parsing file: %s", action_file)

133 objects[action_file] = parser.objects()

134 for k, v in sorted(objects[action_file].items()):

135 if len(v.errors()) > 0: 135 ↛ 136 line 135 didn't jump to line 136 because the condition on line 135 was never true

136 for error in v.errors():

137 logger.error("Error in %s (%s %s): %s", action_file, v.type_name(), k, str(error))

138 has_errors = True

139

140 if has_errors: 140 ↛ 141 line 140 didn't jump to line 141 because the condition on line 140 was never true

141 logger.error("Preflight checks errors. Exiting")

142 sys.exit(1)

143

144 # Second pass:

145 for action_file in namespace.action_files:

146 for _name, obj in sorted(objects[action_file].items()):

147 obj.process(dry_run=namespace.dry_run)

148

149 fails_on = logging.CRITICAL

150 if namespace.fail_on_errors: 150 ↛ 151 line 150 didn't jump to line 151 because the condition on line 150 was never true

151 fails_on = logging.ERROR

152 if namespace.fail_on_warnings: 152 ↛ 154 line 152 didn't jump to line 154 because the condition on line 152 was always true

153 fails_on = logging.WARNING

154 if exit_code_handler.max_levelno >= fails_on: 154 ↛ exit line 154 didn't return from function 'main' because the condition on line 154 was always true

155 sys.exit(1)

156

157

158 if __name__ == "__main__": 158 ↛ 159 line 158 didn't jump to line 159 because the condition on line 158 was never true

159 main()