Coverage for src/gitlabracadabra/cli.py: 72%

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:

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 self._max_levelno = max(self._max_levelno, record.levelno)

76

77 @property

78 def max_levelno(self) -> int:

79 return self._max_levelno

80

81

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

83 argument_parser = _get_argument_parser()

84

85 namespace = argument_parser.parse_args(args)

86

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

88 print(gitlabracadabra.__version__) # noqa: T201

89 sys.exit(0)

90

91 config_files = namespace.config_file

92 gitlab_id = namespace.gitlab

93

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

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

96 else:

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

98 log_level = logging.WARNING

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

100 log_level = logging.INFO

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

102 log_level = logging.DEBUG

103 exit_code_handler = ExitCodeHandler()

104 logging.basicConfig(

105 format=logging_format,

106 level=log_level,

107 )

108 logging.root.addHandler(exit_code_handler)

109

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

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

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

113 sys.exit(0)

114

115 try:

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

117 except Exception as e: # noqa: BLE001

118 logger.error(str(e))

119 sys.exit(1)

120

121 # First pass: Load data and preflight checks

122 objects = {}

123 has_errors = False

124 for action_file in namespace.action_files:

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

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

127 else:

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

129 has_errors = True

130 continue

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

132 objects[action_file] = parser.objects()

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

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

135 for error in v.errors():

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

137 has_errors = True

138

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

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

141 sys.exit(1)

142

143 # Second pass:

144 for action_file in namespace.action_files:

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

146 obj.process(dry_run=namespace.dry_run)

147

148 fails_on = logging.CRITICAL

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

150 fails_on = logging.ERROR

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

152 fails_on = logging.WARNING

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

154 sys.exit(1)

155

156

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

158 main()