Coverage for src/git_dag/cli.py: 100%

79 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-08 12:49 +0200

1#!/usr/bin/env python 

2# PYTHON_ARGCOMPLETE_OK 

3"""Comman-line interface.""" 

4import argparse 

5import logging 

6from typing import Any, Optional 

7 

8import argcomplete 

9 

10from git_dag.constants import CONFIG_FILE 

11from git_dag.git_repository import GitRepository 

12from git_dag.parameters import Params, ParamsPublic, context_ignore_config_file 

13 

14 

15class CustomArgparseNamespace(argparse.Namespace): 

16 """Type hints for argparse arguments. 

17 

18 Note 

19 ----- 

20 The argparse type parameter is a function that converts a string to something, and 

21 raises an error if it can't. It does not add typehints information. 

22 https://stackoverflow.com/q/56441342 

23 

24 """ 

25 

26 path: str 

27 file: str 

28 format: str 

29 init_refs: Optional[list[str]] 

30 max_numb_commits: int 

31 dag_backend: str 

32 log_level: str 

33 

34 html_embed_svg: bool 

35 show_unreachable_commits: bool 

36 show_tags: bool 

37 show_deleted_tags: bool 

38 show_local_branches: bool 

39 show_remote_branches: bool 

40 show_stash: bool 

41 show_trees: bool 

42 show_trees_standalone: bool 

43 show_blobs: bool 

44 show_blobs_standalone: bool 

45 show_head: bool 

46 show_prs_heads: bool 

47 range: Optional[str] 

48 commit_message_as_label: int 

49 xdg_open: bool 

50 

51 

52def get_cla_parser() -> argparse.ArgumentParser: 

53 """Define CLA parser. 

54 

55 Note 

56 ----- 

57 The default value of all flags (``action="store_true"``) is set to ``None`` because 

58 it is used when default values for parameters are being set (see ``parameters.py``). 

59 

60 """ 

61 parser = argparse.ArgumentParser(description="Visualize the git DAG.") 

62 

63 parser.add_argument( 

64 "--config-create", 

65 action="store_true", 

66 default=None, 

67 help=f"Create config {CONFIG_FILE} and exit.", 

68 ) 

69 

70 parser.add_argument( 

71 "--config-ignore", 

72 action="store_true", 

73 default=None, 

74 help=f"Ignore the {CONFIG_FILE} config.", 

75 ) 

76 

77 parser.add_argument( 

78 "-p", 

79 "--path", 

80 help="Path to a git repository.", 

81 ) 

82 

83 parser.add_argument( 

84 "-f", 

85 "--file", 

86 help="Output graphviz file (e.g., `/path/to/file`).", 

87 ) 

88 

89 parser.add_argument( 

90 "-b", 

91 "--dag-backend", 

92 choices=["graphviz"], 

93 help="Backend DAG library.", 

94 ) 

95 

96 parser.add_argument( 

97 "--format", 

98 help=( 

99 "Graphviz output format (tooltips are available only with svg). " 

100 "If the format is set to `gv`, only the graphviz source file is generated." 

101 ), 

102 ) 

103 

104 parser.add_argument( 

105 "-i", 

106 "--init-refs", 

107 nargs="+", 

108 help=( 

109 "A list of branches, tags, git objects (commits, trees, blobs) that " 

110 "represents a limitation from where to display the DAG." 

111 ), 

112 ) 

113 

114 parser.add_argument( 

115 "-R", 

116 dest="range_expr", 

117 help=( 

118 "A range expression (e.g, main..feature). It is passed directly to " 

119 "git rev-list, so any of its flags can be passed as well." 

120 ), 

121 ) 

122 

123 parser.add_argument( 

124 "-n", 

125 "--max-numb-commits", 

126 type=int, 

127 help=( 

128 "Max number of commits to display. If set to 0 and the -i flag is not " 

129 "specified, no limitations are considered whatsoever. If set to n > 0, " 

130 "only n commits reachable from the initial references are displayed (in " 

131 "the absence of user-defined initial references, the output of " 

132 "`git rev-list --all --objects --no-object-names` is used (note that it " 

133 "might not include some unreachable commits." 

134 ), 

135 ) 

136 

137 parser.add_argument( 

138 "-u", 

139 dest="show_unreachable_commits", 

140 action="store_true", 

141 default=None, 

142 help="Show unreachable commits.", 

143 ) 

144 

145 parser.add_argument( 

146 "-t", 

147 dest="show_tags", 

148 action="store_true", 

149 default=None, 

150 help="Show tags.", 

151 ) 

152 

153 parser.add_argument( 

154 "-D", 

155 dest="show_deleted_tags", 

156 action="store_true", 

157 default=None, 

158 help="Show deleted annotated tags.", 

159 ) 

160 

161 parser.add_argument( 

162 "-l", 

163 dest="show_local_branches", 

164 action="store_true", 

165 default=None, 

166 help="Show local branches.", 

167 ) 

168 

169 parser.add_argument( 

170 "-r", 

171 dest="show_remote_branches", 

172 action="store_true", 

173 default=None, 

174 help="Show remote branches.", 

175 ) 

176 

177 parser.add_argument( 

178 "-s", 

179 dest="show_stash", 

180 action="store_true", 

181 default=None, 

182 help="Show stash.", 

183 ) 

184 

185 parser.add_argument( 

186 "-H", 

187 dest="show_head", 

188 action="store_true", 

189 default=None, 

190 help="Show head (has effect only when -l or -r are set as well).", 

191 ) 

192 

193 parser.add_argument( 

194 "-a", 

195 dest="annotations", 

196 action="append", 

197 nargs="+", 

198 default=None, 

199 help=( 

200 "Annotations of refs (can be passed multiple times). The first argument " 

201 "after each -a should be a ref. Subsequent arguments (if any) are joined " 

202 "and placed in the tooltip of the corresponding node." 

203 ), 

204 ) 

205 

206 parser.add_argument( 

207 "--pr", 

208 dest="show_prs_heads", 

209 action="store_true", 

210 default=None, 

211 help=( 

212 "Show pull-requests heads " 

213 "(most of the time this requires passing -u as well)." 

214 ), 

215 ) 

216 

217 parser.add_argument( 

218 "-T", 

219 dest="show_trees", 

220 action="store_true", 

221 default=None, 

222 help="Show trees (WARNING: should be used only with small repositories).", 

223 ) 

224 

225 parser.add_argument( 

226 "--trees-standalone", 

227 dest="show_trees_standalone", 

228 action="store_true", 

229 default=None, 

230 help=( 

231 "Show trees that don't have parent commits reachable from " 

232 "a branch a tag or the reflog." 

233 ), 

234 ) 

235 

236 parser.add_argument( 

237 "-B", 

238 dest="show_blobs", 

239 action="store_true", 

240 default=None, 

241 help="Show blobs (discarded if -T is not set).", 

242 ) 

243 

244 parser.add_argument( 

245 "--blobs-standalone", 

246 dest="show_blobs_standalone", 

247 action="store_true", 

248 default=None, 

249 help=( 

250 "Show blobs that don't have parent commits reachable from " 

251 "a branch a tag or the reflog." 

252 ), 

253 ) 

254 

255 parser.add_argument( 

256 "-m", 

257 "--message", 

258 type=int, 

259 dest="commit_message_as_label", 

260 help=( 

261 "When greater than 0, this is the number of characters from the commit " 

262 "message to use as a commit label. The commit SHA is used otherwise." 

263 ), 

264 ) 

265 

266 parser.add_argument( 

267 "-o", 

268 "--xdg-open", 

269 action="store_true", 

270 default=None, 

271 help="Open output file with xdg-open.", 

272 ) 

273 

274 parser.add_argument( 

275 "--html", 

276 dest="html_embed_svg", 

277 action="store_true", 

278 default=None, 

279 help=( 

280 "Create a standalone HTML file that embeds the generated SVG. " 

281 "Hass effect only when --format is svg." 

282 ), 

283 ) 

284 

285 parser.add_argument( 

286 "--log-level", 

287 choices=["NOTSET", "INFO", "WARNING", "ERROR", "CRITICAL"], 

288 help="Log level.", 

289 ) 

290 

291 return parser 

292 

293 

294def get_user_defined_cla( 

295 raw_args: Optional[list[str]] = None, 

296) -> dict[str, Any]: 

297 """Parse command-line arguments.""" 

298 parser = get_cla_parser() 

299 

300 argcomplete.autocomplete(parser) 

301 args = parser.parse_args(raw_args, namespace=CustomArgparseNamespace()) 

302 return {key: value for key, value in vars(args).items() if value is not None} 

303 

304 

305def main(raw_args: Optional[list[str]] = None) -> None: 

306 """CLI entry poit.""" 

307 user_defined_cla = get_user_defined_cla(raw_args) 

308 

309 # config_ignore and config_create are not stored as parameters 

310 config_ignore = user_defined_cla.pop("config_ignore", False) 

311 config_create = user_defined_cla.pop("config_create", False) 

312 

313 if config_ignore: 

314 with context_ignore_config_file(): 

315 params = Params(public=ParamsPublic(**user_defined_cla)) 

316 else: 

317 params = Params(public=ParamsPublic(**user_defined_cla)) 

318 

319 if config_create: 

320 params.create_config() 

321 return None 

322 

323 logging.getLogger().setLevel(getattr(logging, params.public.log_level)) 

324 GitRepository( 

325 params.public.path, 

326 parse_trees=params.public.show_trees, 

327 ).show(params) 

328 

329 return None 

330 

331 

332if __name__ == "__main__": # pragma: no cover 

333 main()