ai-cr 2.0.0.dev2__py3-none-any.whl → 2.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
gito/cli.py CHANGED
@@ -1,255 +1,217 @@
1
- import asyncio
2
- import logging
3
- import sys
4
- import os
5
- import textwrap
6
- import tempfile
7
- import requests
8
-
9
- import microcore as mc
10
- import typer
11
- from git import Repo
12
-
13
- from .core import review, get_diff, filter_diff
14
- from .report_struct import Report
15
- from .constants import HOME_ENV_PATH
16
- from .bootstrap import bootstrap, app
17
- from .project_config import ProjectConfig
18
- from .utils import no_subcommand, parse_refs_pair
19
-
20
- # Import fix command to register it
21
- from .commands import fix, gh_comment # noqa
22
-
23
-
24
- app_no_subcommand = typer.Typer(pretty_exceptions_show_locals=False)
25
-
26
-
27
- def main():
28
- if sys.platform == "win32":
29
- asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
30
- # Help subcommand alias: if 'help' appears as first non-option arg, replace it with '--help'
31
- if len(sys.argv) > 1 and sys.argv[1] == "help":
32
- sys.argv = [sys.argv[0]] + sys.argv[2:] + ["--help"]
33
-
34
- if no_subcommand(app):
35
- bootstrap()
36
- app_no_subcommand()
37
- else:
38
- app()
39
-
40
-
41
- @app.callback(invoke_without_command=True)
42
- def cli(ctx: typer.Context, verbose: bool = typer.Option(default=False)):
43
- if ctx.invoked_subcommand != "setup":
44
- bootstrap()
45
- if verbose:
46
- mc.logging.LoggingConfig.STRIP_REQUEST_LINES = None
47
-
48
-
49
- def args_to_target(refs, what, against) -> tuple[str | None, str | None]:
50
- _what, _against = parse_refs_pair(refs)
51
- if _what:
52
- if what:
53
- raise typer.BadParameter(
54
- "You cannot specify both 'refs' <WHAT>..<AGAINST> and '--what'. Use one of them."
55
- )
56
- else:
57
- _what = what
58
- if _against:
59
- if against:
60
- raise typer.BadParameter(
61
- "You cannot specify both 'refs' <WHAT>..<AGAINST> and '--against'. Use one of them."
62
- )
63
- else:
64
- _against = against
65
- return _what, _against
66
-
67
-
68
- def arg_refs() -> typer.Argument:
69
- return typer.Argument(
70
- default=None,
71
- help="Git refs to review, [what]..[against] e.g. 'HEAD..HEAD~1'"
72
- )
73
-
74
-
75
- def arg_what() -> typer.Option:
76
- return typer.Option(None, "--what", "-w", help="Git ref to review")
77
-
78
-
79
- def arg_filters() -> typer.Option:
80
- return typer.Option(
81
- "", "--filter", "-f", "--filters",
82
- help="""
83
- filter reviewed files by glob / fnmatch pattern(s),
84
- e.g. 'src/**/*.py', may be comma-separated
85
- """,
86
- )
87
-
88
-
89
- def arg_out() -> typer.Option:
90
- return typer.Option(
91
- None,
92
- "--out", "-o", "--output",
93
- help="Output folder for the code review report"
94
- )
95
-
96
-
97
- def arg_against() -> typer.Option:
98
- return typer.Option(
99
- None,
100
- "--against", "-vs", "--vs",
101
- help="Git ref to compare against"
102
- )
103
-
104
-
105
- @app_no_subcommand.command(name="review", help="Perform code review")
106
- @app.command(name="review", help="Perform code review")
107
- @app.command(name="run", hidden=True)
108
- def cmd_review(
109
- refs: str = arg_refs(),
110
- what: str = arg_what(),
111
- against: str = arg_against(),
112
- filters: str = arg_filters(),
113
- merge_base: bool = typer.Option(default=True, help="Use merge base for comparison"),
114
- out: str = arg_out()
115
- ):
116
- _what, _against = args_to_target(refs, what, against)
117
- asyncio.run(review(
118
- what=_what,
119
- against=_against,
120
- filters=filters,
121
- use_merge_base=merge_base,
122
- out_folder=out,
123
- ))
124
-
125
-
126
- @app.command(help="Configure LLM for local usage interactively")
127
- def setup():
128
- mc.interactive_setup(HOME_ENV_PATH)
129
-
130
-
131
- @app.command(name="render")
132
- @app.command(name="report", hidden=True)
133
- def render(
134
- format: str = typer.Argument(default=Report.Format.CLI),
135
- source: str = typer.Option(
136
- "",
137
- "--src",
138
- "--source",
139
- help="Source file (json) to load the report from"
140
- )
141
- ):
142
- Report.load(file_name=source).to_cli(report_format=format)
143
-
144
-
145
- @app.command(help="Review remote code")
146
- def remote(
147
- url: str = typer.Argument(..., help="Git repository URL"),
148
- refs: str = arg_refs(),
149
- what: str = arg_what(),
150
- against: str = arg_against(),
151
- filters: str = arg_filters(),
152
- merge_base: bool = typer.Option(default=True, help="Use merge base for comparison"),
153
- out: str = arg_out()
154
- ):
155
- _what, _against = args_to_target(refs, what, against)
156
- with tempfile.TemporaryDirectory() as temp_dir:
157
- logging.info(f"Cloning [{mc.ui.green(url)}] to {mc.utils.file_link(temp_dir)} ...")
158
- repo = Repo.clone_from(url, branch=_what, to_path=temp_dir)
159
- asyncio.run(review(
160
- repo=repo,
161
- what=_what,
162
- against=_against,
163
- filters=filters,
164
- use_merge_base=merge_base,
165
- out_folder=out or '.',
166
- ))
167
- repo.close()
168
-
169
-
170
- @app.command(help="Leave a GitHub PR comment with the review.")
171
- def github_comment(
172
- token: str = typer.Option(
173
- os.environ.get("GITHUB_TOKEN", ""), help="GitHub token (or set GITHUB_TOKEN env var)"
174
- ),
175
- ):
176
- """
177
- Leaves a comment with the review on the current GitHub pull request.
178
- """
179
- file = "code-review-report.md"
180
- if not os.path.exists(file):
181
- print(f"Review file not found: {file}")
182
- raise typer.Exit(4)
183
-
184
- with open(file, "r", encoding="utf-8") as f:
185
- body = f.read()
186
-
187
- if not token:
188
- print("GitHub token is required (--token or GITHUB_TOKEN env var).")
189
- raise typer.Exit(1)
190
-
191
- github_env = ProjectConfig.load().prompt_vars["github_env"]
192
- repo = github_env.get("github_repo", "")
193
- pr_env_val = github_env.get("github_pr_number", "")
194
- logging.info(f"github_pr_number = {pr_env_val}")
195
-
196
- # e.g. could be "refs/pull/123/merge" or a direct number
197
- if "/" in pr_env_val and "pull" in pr_env_val:
198
- # refs/pull/123/merge
199
- try:
200
- pr_num_candidate = pr_env_val.strip("/").split("/")
201
- idx = pr_num_candidate.index("pull")
202
- pr = int(pr_num_candidate[idx + 1])
203
- except Exception:
204
- pr = 0
205
- else:
206
- try:
207
- pr = int(pr_env_val)
208
- except Exception:
209
- pr = 0
210
-
211
- api_url = f"https://api.github.com/repos/{repo}/issues/{pr}/comments"
212
- headers = {
213
- "Authorization": f"token {token}",
214
- "Accept": "application/vnd.github+json",
215
- }
216
- data = {"body": body}
217
-
218
- resp = requests.post(api_url, headers=headers, json=data)
219
- if 200 <= resp.status_code < 300:
220
- logging.info(f"Posted review comment to PR #{pr} in {repo}")
221
- else:
222
- logging.error(f"Failed to post comment: {resp.status_code} {resp.reason}\n{resp.text}")
223
- raise typer.Exit(5)
224
-
225
-
226
- @app.command(help="List files in the diff. Might be useful to check what will be reviewed.")
227
- def files(
228
- refs: str = arg_refs(),
229
- what: str = arg_what(),
230
- against: str = arg_against(),
231
- filters: str = arg_filters(),
232
- merge_base: bool = typer.Option(default=True, help="Use merge base for comparison"),
233
- diff: bool = typer.Option(default=False, help="Show diff content")
234
- ):
235
- _what, _against = args_to_target(refs, what, against)
236
- repo = Repo(".")
237
- patch_set = get_diff(repo=repo, what=_what, against=_against, use_merge_base=merge_base)
238
- patch_set = filter_diff(patch_set, filters)
239
- print(
240
- f"Changed files: "
241
- f"{mc.ui.green(_what or 'INDEX')} vs "
242
- f"{mc.ui.yellow(_against or repo.remotes.origin.refs.HEAD.reference.name)}"
243
- f"{' filtered by '+mc.ui.cyan(filters) if filters else ''}"
244
- )
245
- repo.close()
246
- for patch in patch_set:
247
- if patch.is_added_file:
248
- color = mc.ui.green
249
- elif patch.is_removed_file:
250
- color = mc.ui.red
251
- else:
252
- color = mc.ui.blue
253
- print(f"- {color(patch.path)}")
254
- if diff:
255
- print(mc.ui.gray(textwrap.indent(str(patch), " ")))
1
+ import asyncio
2
+ import logging
3
+ import sys
4
+ import textwrap
5
+ import tempfile
6
+
7
+ import microcore as mc
8
+ import typer
9
+ from git import Repo
10
+
11
+ from .core import review, get_diff, filter_diff, answer
12
+ from .report_struct import Report
13
+ from .constants import HOME_ENV_PATH
14
+ from .bootstrap import bootstrap, app
15
+ from .utils import no_subcommand, parse_refs_pair
16
+
17
+ # Import fix command to register it
18
+ from .commands import fix, gh_post_review_comment, gh_react_to_comment, repl # noqa
19
+
20
+
21
+ app_no_subcommand = typer.Typer(pretty_exceptions_show_locals=False)
22
+
23
+
24
+ def main():
25
+ if sys.platform == "win32":
26
+ asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
27
+ # Help subcommand alias: if 'help' appears as first non-option arg, replace it with '--help'
28
+ if len(sys.argv) > 1 and sys.argv[1] == "help":
29
+ sys.argv = [sys.argv[0]] + sys.argv[2:] + ["--help"]
30
+
31
+ if no_subcommand(app):
32
+ bootstrap()
33
+ app_no_subcommand()
34
+ else:
35
+ app()
36
+
37
+
38
+ @app.callback(invoke_without_command=True)
39
+ def cli(ctx: typer.Context, verbose: bool = typer.Option(default=False)):
40
+ if ctx.invoked_subcommand != "setup":
41
+ bootstrap()
42
+ if verbose:
43
+ mc.logging.LoggingConfig.STRIP_REQUEST_LINES = None
44
+
45
+
46
+ def args_to_target(refs, what, against) -> tuple[str | None, str | None]:
47
+ _what, _against = parse_refs_pair(refs)
48
+ if _what:
49
+ if what:
50
+ raise typer.BadParameter(
51
+ "You cannot specify both 'refs' <WHAT>..<AGAINST> and '--what'. Use one of them."
52
+ )
53
+ else:
54
+ _what = what
55
+ if _against:
56
+ if against:
57
+ raise typer.BadParameter(
58
+ "You cannot specify both 'refs' <WHAT>..<AGAINST> and '--against'. Use one of them."
59
+ )
60
+ else:
61
+ _against = against
62
+ return _what, _against
63
+
64
+
65
+ def arg_refs() -> typer.Argument:
66
+ return typer.Argument(
67
+ default=None,
68
+ help="Git refs to review, [what]..[against] e.g. 'HEAD..HEAD~1'"
69
+ )
70
+
71
+
72
+ def arg_what() -> typer.Option:
73
+ return typer.Option(None, "--what", "-w", help="Git ref to review")
74
+
75
+
76
+ def arg_filters() -> typer.Option:
77
+ return typer.Option(
78
+ "", "--filter", "-f", "--filters",
79
+ help="""
80
+ filter reviewed files by glob / fnmatch pattern(s),
81
+ e.g. 'src/**/*.py', may be comma-separated
82
+ """,
83
+ )
84
+
85
+
86
+ def arg_out() -> typer.Option:
87
+ return typer.Option(
88
+ None,
89
+ "--out", "-o", "--output",
90
+ help="Output folder for the code review report"
91
+ )
92
+
93
+
94
+ def arg_against() -> typer.Option:
95
+ return typer.Option(
96
+ None,
97
+ "--against", "-vs", "--vs",
98
+ help="Git ref to compare against"
99
+ )
100
+
101
+
102
+ @app_no_subcommand.command(name="review", help="Perform code review")
103
+ @app.command(name="review", help="Perform code review")
104
+ @app.command(name="run", hidden=True)
105
+ def cmd_review(
106
+ refs: str = arg_refs(),
107
+ what: str = arg_what(),
108
+ against: str = arg_against(),
109
+ filters: str = arg_filters(),
110
+ merge_base: bool = typer.Option(default=True, help="Use merge base for comparison"),
111
+ out: str = arg_out()
112
+ ):
113
+ _what, _against = args_to_target(refs, what, against)
114
+ asyncio.run(review(
115
+ what=_what,
116
+ against=_against,
117
+ filters=filters,
118
+ use_merge_base=merge_base,
119
+ out_folder=out,
120
+ ))
121
+
122
+
123
+ @app.command(name="ask", help="Answer questions about codebase changes")
124
+ @app.command(name="answer", hidden=True)
125
+ @app.command(name="talk", hidden=True)
126
+ def cmd_answer(
127
+ question: str = typer.Argument(help="Question to ask about the codebase changes"),
128
+ refs: str = arg_refs(),
129
+ what: str = arg_what(),
130
+ against: str = arg_against(),
131
+ filters: str = arg_filters(),
132
+ merge_base: bool = typer.Option(default=True, help="Use merge base for comparison"),
133
+ ):
134
+ _what, _against = args_to_target(refs, what, against)
135
+ return answer(
136
+ question=question,
137
+ what=_what,
138
+ against=_against,
139
+ filters=filters,
140
+ use_merge_base=merge_base,
141
+ )
142
+
143
+
144
+ @app.command(help="Configure LLM for local usage interactively")
145
+ def setup():
146
+ mc.interactive_setup(HOME_ENV_PATH)
147
+
148
+
149
+ @app.command(name="render")
150
+ @app.command(name="report", hidden=True)
151
+ def render(
152
+ format: str = typer.Argument(default=Report.Format.CLI),
153
+ source: str = typer.Option(
154
+ "",
155
+ "--src",
156
+ "--source",
157
+ help="Source file (json) to load the report from"
158
+ )
159
+ ):
160
+ Report.load(file_name=source).to_cli(report_format=format)
161
+
162
+
163
+ @app.command(help="Review remote code")
164
+ def remote(
165
+ url: str = typer.Argument(..., help="Git repository URL"),
166
+ refs: str = arg_refs(),
167
+ what: str = arg_what(),
168
+ against: str = arg_against(),
169
+ filters: str = arg_filters(),
170
+ merge_base: bool = typer.Option(default=True, help="Use merge base for comparison"),
171
+ out: str = arg_out()
172
+ ):
173
+ _what, _against = args_to_target(refs, what, against)
174
+ with tempfile.TemporaryDirectory() as temp_dir:
175
+ logging.info(f"Cloning [{mc.ui.green(url)}] to {mc.utils.file_link(temp_dir)} ...")
176
+ repo = Repo.clone_from(url, branch=_what, to_path=temp_dir)
177
+ asyncio.run(review(
178
+ repo=repo,
179
+ what=_what,
180
+ against=_against,
181
+ filters=filters,
182
+ use_merge_base=merge_base,
183
+ out_folder=out or '.',
184
+ ))
185
+ repo.close()
186
+
187
+
188
+ @app.command(help="List files in the diff. Might be useful to check what will be reviewed.")
189
+ def files(
190
+ refs: str = arg_refs(),
191
+ what: str = arg_what(),
192
+ against: str = arg_against(),
193
+ filters: str = arg_filters(),
194
+ merge_base: bool = typer.Option(default=True, help="Use merge base for comparison"),
195
+ diff: bool = typer.Option(default=False, help="Show diff content")
196
+ ):
197
+ _what, _against = args_to_target(refs, what, against)
198
+ repo = Repo(".")
199
+ patch_set = get_diff(repo=repo, what=_what, against=_against, use_merge_base=merge_base)
200
+ patch_set = filter_diff(patch_set, filters)
201
+ print(
202
+ f"Changed files: "
203
+ f"{mc.ui.green(_what or 'INDEX')} vs "
204
+ f"{mc.ui.yellow(_against or repo.remotes.origin.refs.HEAD.reference.name)}"
205
+ f"{' filtered by '+mc.ui.cyan(filters) if filters else ''}"
206
+ )
207
+ repo.close()
208
+ for patch in patch_set:
209
+ if patch.is_added_file:
210
+ color = mc.ui.green
211
+ elif patch.is_removed_file:
212
+ color = mc.ui.red
213
+ else:
214
+ color = mc.ui.blue
215
+ print(f"- {color(patch.path)}")
216
+ if diff:
217
+ print(mc.ui.gray(textwrap.indent(str(patch), " ")))
gito/commands/__init__.py CHANGED
@@ -1 +1 @@
1
- # Command modules register themselves with the CLI app
1
+ # Command modules register themselves with the CLI app