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
« 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/>.
19from __future__ import annotations
21import logging
22import sys
23from argparse import ArgumentParser
24from typing import TYPE_CHECKING
26import gitlabracadabra
27import gitlabracadabra.parser
28from gitlabracadabra.gitlab.connections import GitlabConnections
30if TYPE_CHECKING:
31 from collections.abc import Sequence
33logger = logging.getLogger(__name__)
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 )
66 return parser
69class ExitCodeHandler(logging.Handler):
70 def __init__(self) -> None:
71 logging.Handler.__init__(self)
72 self._max_levelno: int = logging.NOTSET
74 def emit(self, record: logging.LogRecord) -> None:
75 self._max_levelno = max(self._max_levelno, record.levelno)
77 @property
78 def max_levelno(self) -> int:
79 return self._max_levelno
82def main(args: Sequence[str] | None = None) -> None:
83 argument_parser = _get_argument_parser()
85 namespace = argument_parser.parse_args(args)
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)
91 config_files = namespace.config_file
92 gitlab_id = namespace.gitlab
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)
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)
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)
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
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)
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)
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)
157if __name__ == "__main__": 157 ↛ 158line 157 didn't jump to line 158 because the condition on line 157 was never true
158 main()