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

90 statements  

« prev     ^ index     » next       coverage.py v7.15.0, created at 2026-07-07 22:55 +0200

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 

19from __future__ import annotations 

20 

21import logging 

22import sys 

23from argparse import ArgumentParser 

24from typing import TYPE_CHECKING 

25 

26import gitlabracadabra 

27import gitlabracadabra.parser 

28from gitlabracadabra.gitlab.connections import GitlabConnections 

29 

30if TYPE_CHECKING: 

31 from collections.abc import Sequence 

32 

33logger = logging.getLogger(__name__) 

34 

35 

36def _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 

69class 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 

82def 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 

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

158 main()