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
« 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
8import argcomplete
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
15class CustomArgparseNamespace(argparse.Namespace):
16 """Type hints for argparse arguments.
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
24 """
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
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
52def get_cla_parser() -> argparse.ArgumentParser:
53 """Define CLA parser.
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``).
60 """
61 parser = argparse.ArgumentParser(description="Visualize the git DAG.")
63 parser.add_argument(
64 "--config-create",
65 action="store_true",
66 default=None,
67 help=f"Create config {CONFIG_FILE} and exit.",
68 )
70 parser.add_argument(
71 "--config-ignore",
72 action="store_true",
73 default=None,
74 help=f"Ignore the {CONFIG_FILE} config.",
75 )
77 parser.add_argument(
78 "-p",
79 "--path",
80 help="Path to a git repository.",
81 )
83 parser.add_argument(
84 "-f",
85 "--file",
86 help="Output graphviz file (e.g., `/path/to/file`).",
87 )
89 parser.add_argument(
90 "-b",
91 "--dag-backend",
92 choices=["graphviz"],
93 help="Backend DAG library.",
94 )
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 )
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 )
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 )
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 )
137 parser.add_argument(
138 "-u",
139 dest="show_unreachable_commits",
140 action="store_true",
141 default=None,
142 help="Show unreachable commits.",
143 )
145 parser.add_argument(
146 "-t",
147 dest="show_tags",
148 action="store_true",
149 default=None,
150 help="Show tags.",
151 )
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 )
161 parser.add_argument(
162 "-l",
163 dest="show_local_branches",
164 action="store_true",
165 default=None,
166 help="Show local branches.",
167 )
169 parser.add_argument(
170 "-r",
171 dest="show_remote_branches",
172 action="store_true",
173 default=None,
174 help="Show remote branches.",
175 )
177 parser.add_argument(
178 "-s",
179 dest="show_stash",
180 action="store_true",
181 default=None,
182 help="Show stash.",
183 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
285 parser.add_argument(
286 "--log-level",
287 choices=["NOTSET", "INFO", "WARNING", "ERROR", "CRITICAL"],
288 help="Log level.",
289 )
291 return parser
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()
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}
305def main(raw_args: Optional[list[str]] = None) -> None:
306 """CLI entry poit."""
307 user_defined_cla = get_user_defined_cla(raw_args)
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)
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))
319 if config_create:
320 params.create_config()
321 return None
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)
329 return None
332if __name__ == "__main__": # pragma: no cover
333 main()