Coverage for src/features/gcil.py: 50.67%

75 statements  

« prev     ^ index     » next       coverage.py v7.17.2, created at 2026-11-08 13:49 +0000

1#!/usr/bin/env python3 

2 

3# Standard libraries 

4from pathlib import Path 

5from re import search 

6from typing import List 

7 

8# Components 

9from ..features.git import Git 

10from ..features.precommit import PreCommit 

11from ..prints.colors import Colors 

12from ..system.commands import Commands 

13from ..system.platform import Platform 

14 

15# Gcil class 

16class Gcil: 

17 

18 # Constants 

19 BADGE_IMAGE: str = 'https://img.shields.io/badge/gcil-enabled-brightgreen?logo=gitlab' 

20 BADGE_URL: str = 'https://radiandevcore.gitlab.io/tools/gcil' 

21 BADGE_TEXT: str = 'gcil' 

22 BINARY: str = 'gcil' 

23 EOL: str = '\n' 

24 FILE: str = '.gitlab-ci.yml' 

25 HOOK_DESCRIPTION: str = 'Automatically run GitLab CI job with gcil' 

26 HOOK_ID: str = 'run-gcil-push' 

27 HOOK_NAME: str = 'Run GitLab CI job with gcil' 

28 HOOKS_LABEL: str = 'gcil' 

29 HOOKS_URL: str = 'https://gitlab.com/RadianDevCore/tools/gcil' 

30 HOOKS_VERSION_MINIMAL: str = '13.1.0' 

31 MARKER_NODE: str = '.local:' 

32 MARKER_VARIABLE: str = 'CI_LOCAL' 

33 

34 # Jobs 

35 JOBS_WHITELIST: List[str] = [ 

36 r'codestyle[:]?.*', 

37 r'lint[:]?.*', 

38 r'typings[:]?.*', 

39 ] 

40 

41 # Configure unused hooks, pylint: disable=too-many-branches,too-many-statements 

42 @staticmethod 

43 def configure_hooks( 

44 configuration_file: str, 

45 autoupdate: bool, 

46 ) -> bool: 

47 

48 # Validate gcil support 

49 if not Gcil.exists() or not Gcil.installed(): 

50 return False 

51 

52 # Validate pre-commit support 

53 if not Path(configuration_file).exists(): 

54 return False # pragma: no cover 

55 

56 # Configure pre-commit unused hooks 

57 print(' ') 

58 print(f'{Colors.GREEN} ===[' 

59 f'{Colors.YELLOW} Configure sources: ' 

60 f'{Colors.YELLOW_LIGHT}{Gcil.HOOK_ID}' 

61 f'{Colors.GREEN} ]===' 

62 f'{Colors.RESET}') 

63 print(' ') 

64 

65 # Detect supported jobs 

66 jobs_list: List[str] = Gcil.jobs_list() 

67 jobs_supported: List[str] = [] 

68 for job in jobs_list: 

69 if any(search(fr'^{whitelist}$', job) for whitelist in Gcil.JOBS_WHITELIST): 

70 jobs_supported += [job] 

71 

72 # Show supported jobs hooks 

73 print( 

74 f'detected supported jobs hooks in configuration: {", ".join(jobs_supported) or "-"}' 

75 ) 

76 Platform.flush() 

77 

78 # Append hooks configuration 

79 if jobs_supported: 

80 with open( 

81 configuration_file, 

82 encoding='utf8', 

83 mode='a', 

84 ) as file: 

85 

86 # Inject repository header 

87 file.write( 

88 PreCommit.CONFIGURATION_EOL.join([ 

89 '', 

90 f' # Repository: {Gcil.HOOKS_LABEL}', 

91 f' - repo: {Gcil.HOOKS_URL}', 

92 f' rev: {Gcil.HOOKS_VERSION_MINIMAL}', 

93 ' hooks:', 

94 '', 

95 ])) 

96 

97 # Inject repository hooks 

98 for job in jobs_supported: 

99 file.write( 

100 PreCommit.CONFIGURATION_EOL.join([ 

101 f' - id: {Gcil.HOOK_ID}', 

102 f' name: {Gcil.HOOK_NAME} ({job})', 

103 f' description: {Gcil.HOOK_DESCRIPTION} ({job})', 

104 ' args:', 

105 f" - '{job}'", 

106 '', 

107 ])) 

108 

109 # Autoupdate repository 

110 if autoupdate and Git.is_repository(): 

111 print(f'updating {Gcil.HOOKS_LABEL} repository with pre-commit: ', end='') 

112 Platform.flush() 

113 PreCommit.configure() 

114 PreCommit.autoupdate(Gcil.HOOKS_URL) 

115 Platform.flush() 

116 

117 # Result 

118 return True 

119 

120 # Configured 

121 @staticmethod 

122 def configured() -> bool: 

123 

124 # Ignore missing file 

125 if not Gcil.exists(): 

126 return False 

127 

128 # Detect gcil configurations 

129 return Commands.grep( 

130 Path(Gcil.FILE), 

131 Gcil.MARKER_NODE, 

132 ) or Commands.grep( 

133 Path(Gcil.FILE), 

134 Gcil.MARKER_VARIABLE, 

135 ) 

136 

137 # Jobs list 

138 @staticmethod 

139 def jobs_list() -> List[str]: 

140 

141 # Validate gcil support 

142 if not Gcil.exists() or not Gcil.installed(): 

143 return [] 

144 

145 # Dump jobs configuration 

146 gcil_dump: str = Commands.output(Gcil.BINARY, [ 

147 '--dump', 

148 ]) 

149 if not gcil_dump or gcil_dump == '{}': 

150 return [] 

151 

152 # Extract job names 

153 jobs: List[str] = [] 

154 for line in [ 

155 line for line in gcil_dump.splitlines() 

156 if line.strip() and not line.startswith(' ') 

157 ]: 

158 job: str = line.strip().rstrip(':') 

159 if not job.startswith('.'): 

160 jobs += [job] 

161 

162 # Result 

163 return jobs 

164 

165 # Exists 

166 @staticmethod 

167 def exists() -> bool: 

168 

169 # Check if configuration exists 

170 return Path(Gcil.FILE).exists() 

171 

172 # Installed 

173 @staticmethod 

174 def installed() -> bool: 

175 

176 # Check installed gcil 

177 return Commands.exists(Gcil.BINARY)