Coverage for src/cli/main.py: 93.16%

117 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-21 21:33 +0000

1#!/usr/bin/env python3 

2 

3# Standard libraries 

4from argparse import ( 

5 _ArgumentGroup, 

6 _MutuallyExclusiveGroup, 

7 ArgumentParser, 

8 Namespace, 

9 RawTextHelpFormatter, 

10 SUPPRESS, 

11) 

12from os import environ 

13from pathlib import Path 

14from shutil import get_terminal_size 

15from sys import exit as sys_exit 

16 

17# Components 

18from ..package.bundle import Bundle 

19from ..package.settings import Settings 

20from ..package.updates import Updates 

21from ..package.version import Version 

22from ..prints.colors import Colors 

23from ..system.platform import Platform 

24from ..types.environments import Environments 

25from ..types.gitlab import MigrationEntities 

26from ..types.paths import Paths 

27from .entrypoint import Entrypoint 

28 

29# Constants 

30HELP_POSITION: int = 29 

31 

32# Main, pylint: disable=too-many-branches,too-many-statements 

33def main() -> None: 

34 

35 # Variables 

36 environments: Environments 

37 group: _ArgumentGroup 

38 result: Entrypoint.Result = Entrypoint.Result.ERROR 

39 subgroup: _MutuallyExclusiveGroup 

40 

41 # Configure environment variables 

42 environments = Environments() 

43 environments.group = 'environment variables' 

44 environments.add( 

45 'gitlab_input_token', 

46 Bundle.ENV_GITLAB_INPUT_TOKEN, 

47 'Input GitLab API token environment variable', 

48 Bundle.ENV_GITLAB_TOKEN, 

49 ) 

50 environments.add( 

51 'gitlab_output_token', 

52 Bundle.ENV_GITLAB_OUTPUT_TOKEN, 

53 'Output GitLab API token environment variable', 

54 Bundle.ENV_GITLAB_TOKEN, 

55 ) 

56 environments.add( 

57 'ci_job_token', 

58 Bundle.ENV_CI_JOB_TOKEN, 

59 'GitLab CI job token environment variable (CI only)', 

60 ) 

61 

62 # Arguments creation 

63 parser: ArgumentParser = ArgumentParser( 

64 prog=Bundle.NAME, 

65 description=f'{Bundle.NAME}: {Bundle.DESCRIPTION}', 

66 epilog=environments.help(HELP_POSITION), 

67 add_help=False, 

68 formatter_class=lambda prog: RawTextHelpFormatter( 

69 prog, 

70 max_help_position=HELP_POSITION, 

71 width=min( 

72 120, 

73 get_terminal_size().columns - 2, 

74 ), 

75 ), 

76 ) 

77 

78 # Arguments internal definitions 

79 group = parser.add_argument_group('internal arguments') 

80 group.add_argument( 

81 '-h', 

82 '--help', 

83 dest='help', 

84 action='store_true', 

85 help='Show this help message', 

86 ) 

87 group.add_argument( 

88 '--version', 

89 dest='version', 

90 action='store_true', 

91 help='Show the current version', 

92 ) 

93 group.add_argument( 

94 '--no-color', 

95 dest='no_color', 

96 action='store_true', 

97 help=f'Disable colors outputs with \'{Bundle.ENV_NO_COLOR}=1\'\n' 

98 '(or default settings: [themes] > no_color)', 

99 ) 

100 group.add_argument( 

101 '--update-check', 

102 dest='update_check', 

103 action='store_true', 

104 help='Check for newer package updates', 

105 ) 

106 group.add_argument( 

107 '--settings', 

108 dest='settings', 

109 action='store_true', 

110 help='Show the current settings path and contents', 

111 ) 

112 group.add_argument( 

113 '--set', 

114 dest='set', 

115 action='store', 

116 metavar=('GROUP', 'KEY', 'VAL'), 

117 nargs=3, 

118 help='Set settings specific \'VAL\' value to [GROUP] > KEY\n' \ 

119 'or unset by using \'UNSET\' as \'VAL\'', 

120 ) 

121 

122 # Arguments credentials definitions 

123 group = parser.add_argument_group('credentials arguments') 

124 group.add_argument( 

125 '-c', 

126 '--config', 

127 dest='configs', 

128 action='append', 

129 metavar='FILES', 

130 help=f'Python GitLab configuration files' 

131 f' (default: {Bundle.ENV_PYTHON_GITLAB_CFG} environment)', 

132 ) 

133 

134 # Arguments migration definitions 

135 group = parser.add_argument_group('migration arguments') 

136 group.add_argument( 

137 '--archive-exports', 

138 dest='archive_exports', 

139 action='store', 

140 metavar='FOLDER', 

141 help='Store exported projects and groups to a folder', 

142 ) 

143 subgroup = group.add_mutually_exclusive_group() 

144 subgroup.add_argument( 

145 '--archive-sources', 

146 dest='archive_sources', 

147 action='store_true', 

148 help='Archive sources after successful migration', 

149 ) 

150 subgroup.add_argument( 

151 '--delete-sources', 

152 dest='delete_sources', 

153 action='store_true', 

154 help='Delete sources after successful migration', 

155 ) 

156 group.add_argument( 

157 '--dry-run', 

158 dest='dry_run', 

159 action='store_true', 

160 help='Enable dry run mode to check without saving', 

161 ) 

162 group.add_argument( 

163 '--exclude-group', 

164 dest='exclude_group', 

165 action='store_true', 

166 help='Exclude parent group migration', 

167 ) 

168 group.add_argument( 

169 '--exclude-subgroups', 

170 dest='exclude_subgroups', 

171 action='store_true', 

172 help='Exclude children subgroups migration', 

173 ) 

174 group.add_argument( 

175 '--exclude-projects', 

176 dest='exclude_projects', 

177 action='store_true', 

178 help='Exclude children projects migration', 

179 ) 

180 group.add_argument( 

181 '--flatten-group', 

182 dest='flatten_group', 

183 action='store_true', 

184 help='Flatten group projects upon migration', 

185 ) 

186 group.add_argument( 

187 '--migrate-packages', 

188 dest='migrate_packages', 

189 action='store_true', 

190 help='Migrate input GitLab Packages to output GitLab projects', 

191 ) 

192 group.add_argument( 

193 '--confirm', 

194 dest='confirm', 

195 action='store_true', 

196 help='Automatically confirm all removal and contents warnings', 

197 ) 

198 group.add_argument( 

199 '--overwrite', 

200 dest='overwrite', 

201 action='store_true', 

202 help='Overwrite existing projects on output GitLab', 

203 ) 

204 group.add_argument( 

205 '--rename-project', 

206 dest='rename_project', 

207 action='store', 

208 metavar='NAME', 

209 help='Rename GitLab output project (only for single input project)', 

210 ) 

211 group.add_argument( 

212 '--normalize-names', 

213 dest='normalize_names', 

214 action='store_true', 

215 help='Normalize GitLab output group and project names', 

216 ) 

217 group.add_argument( 

218 '--available-entities', 

219 dest='available_entities', 

220 action='store_true', 

221 help='List the available GitLab export/import entities known by the tool', 

222 ) 

223 group.add_argument( 

224 '--reset-entities', 

225 dest='reset_entities', 

226 default=','.join(MigrationEntities.defaults()), 

227 metavar='ENTITIES', 

228 help='List of GitLab export/import entities to reset separated by ","' 

229 ' (default: %(default)s)', 

230 ) 

231 

232 # Arguments general settings definitions 

233 group = parser.add_argument_group('general settings arguments') 

234 group.add_argument( 

235 '--set-avatar', 

236 dest='set_avatar', 

237 action='store', 

238 metavar='FILE', 

239 help='Set avatar of GitLab output projects and groups', 

240 ) 

241 group.add_argument( 

242 '--update-descriptions', 

243 dest='update_description', 

244 action='store_true', 

245 help='Update description of GitLab output projects and groups automatically', 

246 ) 

247 

248 # Arguments hidden definitions 

249 group = parser.add_argument_group('hidden arguments') 

250 group.add_argument( 

251 '--archive-exports-dir', 

252 dest='archive_exports_dir', 

253 action='store', 

254 default=None, 

255 help=SUPPRESS, 

256 ) 

257 

258 # Arguments positional definitions 

259 group = parser.add_argument_group('positional arguments') 

260 group.add_argument( 

261 '--', 

262 dest='double_dash', 

263 action='store_true', 

264 help='Positional arguments separator (recommended)', 

265 ) 

266 group.add_argument( 

267 dest='input_url_path', 

268 action='store', 

269 nargs='?', 

270 help='Input GitLab group or project path URL', 

271 ) 

272 group.add_argument( 

273 dest='output_url_namespace', 

274 action='store', 

275 nargs='?', 

276 help='Output GitLab group or user namespace URL', 

277 ) 

278 group.add_argument( 

279 dest='rename_project_positional', 

280 action='store', 

281 nargs='?', 

282 metavar='rename_single_project', 

283 help='Rename GitLab output project (only for single input project)', 

284 ) 

285 

286 # Arguments parser 

287 options: Namespace = parser.parse_args() 

288 

289 # Help informations 

290 if options.help: 

291 print(' ') 

292 parser.print_help() 

293 print(' ') 

294 Platform.flush() 

295 sys_exit(0) 

296 

297 # Instantiate settings 

298 settings: Settings = Settings(name=Bundle.NAME) 

299 

300 # Prepare no_color 

301 if not options.no_color: 

302 if settings.has('themes', 'no_color'): 

303 options.no_color = settings.get_bool('themes', 'no_color') 

304 else: 

305 options.no_color = False 

306 settings.set_bool('themes', 'no_color', options.no_color) 

307 

308 # Configure no_color 

309 if options.no_color: 

310 environ[Bundle.ENV_FORCE_COLOR] = '0' 

311 environ[Bundle.ENV_NO_COLOR] = '1' 

312 

313 # Prepare colors 

314 Colors.prepare() 

315 

316 # Settings setter 

317 if options.set: 

318 settings.set(options.set[0], options.set[1], options.set[2]) 

319 settings.show() 

320 sys_exit(0) 

321 

322 # Settings informations 

323 if options.settings: 

324 settings.show() 

325 sys_exit(0) 

326 

327 # Instantiate updates 

328 updates: Updates = Updates( 

329 name=Bundle.PACKAGE, 

330 settings=settings, 

331 ) 

332 

333 # Version informations 

334 if options.version: 

335 print( 

336 f'{Bundle.NAME} {Version.get()} from {Version.path()} (python {Version.python()})' 

337 ) 

338 Platform.flush() 

339 sys_exit(0) 

340 

341 # Check for current updates 

342 if options.update_check: 

343 if not updates.check(): 

344 updates.check(older=True) 

345 sys_exit(0) 

346 

347 # Adapt rename project arguments 

348 if options.rename_project_positional and not options.rename_project: 

349 options.rename_project = options.rename_project_positional 

350 

351 # Arguments validation 

352 if options.input_url_path and not options.output_url_namespace and ( 

353 options.archive_exports # Export input to archives 

354 or options.archive_sources # Archive input sources 

355 or options.delete_sources # Delete input sources 

356 ): 

357 pass 

358 elif not options.input_url_path or not options.output_url_namespace: 

359 result = Entrypoint.Result.CRITICAL 

360 

361 # Prepare archive exports 

362 if options.archive_exports: 

363 options.archive_exports_dir = Paths.resolve(Path('.') / options.archive_exports) 

364 Path(options.archive_exports_dir).mkdir(parents=True, exist_ok=True) 

365 

366 # Header 

367 print(' ') 

368 Platform.flush() 

369 

370 # Tool identifier 

371 if result != Entrypoint.Result.CRITICAL: 

372 print(f'{Colors.BOLD} {Bundle.NAME}' 

373 f'{Colors.YELLOW_LIGHT} ({Version.get()})' 

374 f'{Colors.RESET}') 

375 Platform.flush() 

376 

377 # CLI entrypoint 

378 if result != Entrypoint.Result.CRITICAL: 

379 result = Entrypoint.cli( 

380 options, 

381 environments, 

382 ) 

383 

384 # CLI helper 

385 else: 

386 parser.print_help() 

387 

388 # Footer 

389 print(' ') 

390 Platform.flush() 

391 

392 # Check for daily updates 

393 if updates.enabled and updates.daily: 

394 updates.check() 

395 

396 # Result 

397 if result in [ 

398 Entrypoint.Result.SUCCESS, 

399 Entrypoint.Result.FINALIZE, 

400 ]: 

401 sys_exit(0) 

402 else: 

403 sys_exit(1) 

404 

405# Entrypoint 

406if __name__ == '__main__': # pragma: no cover 

407 main()