ai-cr 2.0.2__py3-none-any.whl → 3.0.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ai-cr
3
- Version: 2.0.2
3
+ Version: 3.0.0
4
4
  Summary: AI code review tool that works with any language model provider. It detects issues in GitHub pull requests or local changes—instantly, reliably, and without vendor lock-in.
5
5
  License: MIT
6
6
  Keywords: static code analysis,code review,code quality,ai,coding,assistant,llm,github,automation,devops,developer tools,github actions,workflows,git
@@ -16,8 +16,8 @@ Classifier: Programming Language :: Python :: 3.12
16
16
  Classifier: Programming Language :: Python :: 3.13
17
17
  Classifier: Topic :: Software Development
18
18
  Requires-Dist: GitPython (>=3.1.44,<4.0.0)
19
- Requires-Dist: ai-microcore (==4.0.0)
20
- Requires-Dist: anthropic (>=0.52.2,<0.53.0)
19
+ Requires-Dist: ai-microcore (==4.2.1)
20
+ Requires-Dist: anthropic (>=0.57.1,<0.58.0)
21
21
  Requires-Dist: ghapi (>=1.0.6,<1.1.0)
22
22
  Requires-Dist: google-generativeai (>=0.8.5,<0.9.0)
23
23
  Requires-Dist: jira (>=3.8.0,<4.0.0)
@@ -87,7 +87,7 @@ jobs:
87
87
  uses: actions/setup-python@v5
88
88
  with: { python-version: "3.13" }
89
89
  - name: Install AI Code Review tool
90
- run: pip install gito.bot~=2.0
90
+ run: pip install gito.bot~=3.0
91
91
  - name: Run AI code analysis
92
92
  env:
93
93
  LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
@@ -0,0 +1,36 @@
1
+ gito/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ gito/__main__.py,sha256=MSmt_5Xg84uHqzTN38JwgseJK8rsJn_11A8WD99VtEo,61
3
+ gito/bootstrap.py,sha256=9MCV80dMs8QFaB0tmuSlrpPbnRu2ACPHAWKnOSQV-xI,3168
4
+ gito/cli.py,sha256=H6LpoUfw-R4BdJcG4WGcktaIlq2C1kgKWIZ7IEwFljI,6808
5
+ gito/cli_base.py,sha256=h0CvkfOaIYGArqNuKyAuDcJDx-hOB7H7uyfJV3afINg,2390
6
+ gito/commands/__init__.py,sha256=B2uUQsLMEsHfNT1N3lWYm38WSuQIHFmjiGs2tdBuDBA,55
7
+ gito/commands/deploy.py,sha256=PbNcw7Ccau2Bncpi121YDRz9gg9xvkrviytrAEFfpmw,3749
8
+ gito/commands/fix.py,sha256=C4imdS776yl7g-KmH9Wu2PqeN4WNJh8NvSeV3pV-63g,5304
9
+ gito/commands/gh_post_review_comment.py,sha256=xrCauuifrUEufBjx43sw5FMtWP9seHiGYzwAq5SFnvQ,3795
10
+ gito/commands/gh_react_to_comment.py,sha256=3R0Ke-Met06epooImI6ZC2TuLQ2Qpv276OWMSelVcNY,6395
11
+ gito/commands/linear_comment.py,sha256=8kCudz0cogWm8LsgLRWN-BybDq13zEqwV456JB3_7xo,1411
12
+ gito/commands/repl.py,sha256=jJCRO-O7r_fFXZrk9f1_oAcYPOAwPi3cGI3bMoRyer8,549
13
+ gito/config.toml,sha256=kd2fbztnr2LGaNE2BE81yt1Ed0PTF5xe_8WtTgDxxkw,18417
14
+ gito/constants.py,sha256=_G40SAMfuYjYsBzEVUIxKT8Vo0dAWIQqCWWxdaBAltY,766
15
+ gito/context.py,sha256=OBfcQOREsNx8WHANsplNrnrKYrXz1PyZyne11lSfZjw,446
16
+ gito/core.py,sha256=Huz0WnnuuojwFWKD_zPLIjB6wzcoGw3r7a4zzDiQLBI,14484
17
+ gito/env.py,sha256=HWNQb2q1mzyTg6w1FmDIxUheNSuoIUEsIEPiC1SYaYY,61
18
+ gito/gh_api.py,sha256=FbJ7w6dtuAFUVbZNyNHn8RRDGBvJyQlrIX82m46cTac,2958
19
+ gito/issue_trackers.py,sha256=XYspyaIuf0ANQSvDUea5_oOdo9tQvpZZsapI5S9g78U,1551
20
+ gito/pipeline.py,sha256=H6eiyDHUg_p3jxNVhSnyB7EVVNbMIZkA35WMymgPnh0,2610
21
+ gito/pipeline_steps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
+ gito/pipeline_steps/jira.py,sha256=NjFgpGFAkT5PSXi6jQ9Yr8CY9M8sa3_rMb7RFhdpoNg,1862
23
+ gito/pipeline_steps/linear.py,sha256=6UDc8nGKGpwHruPq8VItE2QBWshWxaTapoMhu_qjN_g,2445
24
+ gito/project_config.py,sha256=jOPVMgN01_krELlNXzZlrZOKmXZP-RLDS7iOgVSUyic,3182
25
+ gito/report_struct.py,sha256=96gDYnw0MXhOZrrGaNOTyWstqpjjS7_cuWufd0XJzR4,4320
26
+ gito/tpl/github_workflows/components/env-vars.j2,sha256=ouZY4vpKUkAAM5qUxA3BKAh9BpJMfwsrn8qqahhrh7U,356
27
+ gito/tpl/github_workflows/components/installs.j2,sha256=j5wl0yVEIrXZDpAgzqBwmhXQA9End3xFspPxr2ZzHR0,693
28
+ gito/tpl/github_workflows/gito-code-review.yml.j2,sha256=PcxXMTblWX2ANK-8bkxFPI3H-xby5Do_yaufhxGzw0Y,1159
29
+ gito/tpl/github_workflows/gito-react-to-comments.yml.j2,sha256=zVNkJRgje75AejXAePZugruDu3pOuDaPcujrDfth8K4,2165
30
+ gito/tpl/release_notes.j2,sha256=20QtQwdZYcFEjmfYxTysenY-njTPl2nmHpv9077WFdg,849
31
+ gito/utils.py,sha256=OSBn7IeWKjoLJoGOcdgQcJHBA69UiHUChtaYMInUeko,6852
32
+ ai_cr-3.0.0.dist-info/LICENSE,sha256=VbdF_GbbDK24JvdTfnsxa2M6jmhsxmRSFeHCx-lICGE,1075
33
+ ai_cr-3.0.0.dist-info/METADATA,sha256=HzNuuuJhSDq9LwSRuzx0QUOHQRhC7-p3KX6XmAoYH_c,7989
34
+ ai_cr-3.0.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
35
+ ai_cr-3.0.0.dist-info/entry_points.txt,sha256=Ua1DxkhJJ8TZuLgnH-IlWCkrre_0S0dq_GtYRaYupWk,38
36
+ ai_cr-3.0.0.dist-info/RECORD,,
gito/bootstrap.py CHANGED
@@ -1,15 +1,18 @@
1
- import logging
2
1
  import os
2
+ import sys
3
+ import io
4
+ import logging
3
5
  from datetime import datetime
6
+ from pathlib import Path
4
7
 
5
8
  import microcore as mc
6
- import typer
7
9
 
8
10
  from .utils import is_running_in_github_action
9
- from .constants import HOME_ENV_PATH, EXECUTABLE
11
+ from .constants import HOME_ENV_PATH, EXECUTABLE, PROJECT_GITO_FOLDER
12
+ from .env import Env
10
13
 
11
14
 
12
- def setup_logging():
15
+ def setup_logging(log_level: int = logging.INFO):
13
16
  class CustomFormatter(logging.Formatter):
14
17
  def format(self, record):
15
18
  dt = datetime.fromtimestamp(record.created).strftime("%Y-%m-%d %H:%M:%S")
@@ -24,19 +27,41 @@ def setup_logging():
24
27
 
25
28
  handler = logging.StreamHandler()
26
29
  handler.setFormatter(CustomFormatter())
27
- logging.basicConfig(level=logging.INFO, handlers=[handler])
30
+ logging.basicConfig(level=log_level, handlers=[handler])
28
31
 
29
32
 
30
- def bootstrap():
33
+ def bootstrap(verbosity: int = 1):
31
34
  """Bootstrap the application with the environment configuration."""
32
- setup_logging()
33
- logging.info("Bootstrapping...")
35
+ log_levels_by_verbosity = {
36
+ 0: logging.CRITICAL,
37
+ 1: logging.INFO,
38
+ 2: logging.DEBUG,
39
+ 3: logging.DEBUG,
40
+ }
41
+ Env.verbosity = verbosity
42
+ Env.logging_level = log_levels_by_verbosity.get(verbosity, logging.INFO)
43
+ setup_logging(Env.logging_level)
44
+ logging.info("Bootstrapping... "+mc.ui.gray(f"[verbosity={verbosity}]"))
45
+
46
+ # cp1251 is used on Windows when redirecting output
47
+ if sys.stdout.encoding and sys.stdout.encoding.lower() != "utf-8":
48
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
49
+
34
50
  try:
35
51
  mc.configure(
36
52
  DOT_ENV_FILE=HOME_ENV_PATH,
37
- USE_LOGGING=True,
53
+ USE_LOGGING=verbosity >= 1,
38
54
  EMBEDDING_DB_TYPE=mc.EmbeddingDbType.NONE,
55
+ PROMPT_TEMPLATES_PATH=[
56
+ PROJECT_GITO_FOLDER,
57
+ Path(__file__).parent / "tpl"
58
+ ],
39
59
  )
60
+ if verbosity > 1:
61
+ mc.logging.LoggingConfig.STRIP_REQUEST_LINES = None
62
+ else:
63
+ mc.logging.LoggingConfig.STRIP_REQUEST_LINES = [300, 15]
64
+
40
65
  except mc.LLMConfigError as e:
41
66
  msg = str(e)
42
67
  if is_running_in_github_action():
@@ -60,7 +85,3 @@ def bootstrap():
60
85
  except Exception as e:
61
86
  logging.error(f"Unexpected configuration error: {e}")
62
87
  raise SystemExit(3)
63
- mc.logging.LoggingConfig.STRIP_REQUEST_LINES = [300, 15]
64
-
65
-
66
- app = typer.Typer(pretty_exceptions_show_locals=False)
gito/cli.py CHANGED
@@ -1,22 +1,34 @@
1
+ import os
1
2
  import asyncio
2
3
  import logging
3
4
  import sys
4
5
  import textwrap
5
- import tempfile
6
6
 
7
7
  import microcore as mc
8
8
  import typer
9
9
  from git import Repo
10
10
 
11
11
  from .core import review, get_diff, filter_diff, answer
12
+ from .cli_base import (
13
+ app,
14
+ args_to_target,
15
+ arg_refs,
16
+ arg_what,
17
+ arg_filters,
18
+ arg_out,
19
+ arg_against,
20
+ get_repo_context,
21
+ )
12
22
  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
23
+ from .constants import HOME_ENV_PATH, GITHUB_MD_REPORT_FILE_NAME
24
+ from .bootstrap import bootstrap
25
+ from .utils import no_subcommand, extract_gh_owner_repo, remove_html_comments
26
+ from .gh_api import resolve_gh_token
16
27
 
17
28
  # Import fix command to register it
18
- from .commands import fix, gh_post_review_comment, gh_react_to_comment, repl # noqa
19
-
29
+ from .commands import fix, gh_react_to_comment, repl, deploy # noqa
30
+ from .commands.gh_post_review_comment import post_github_cr_comment
31
+ from .commands.linear_comment import linear_comment
20
32
 
21
33
  app_no_subcommand = typer.Typer(pretty_exceptions_show_locals=False)
22
34
 
@@ -36,67 +48,26 @@ def main():
36
48
 
37
49
 
38
50
  @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(
51
+ def cli(
52
+ ctx: typer.Context,
53
+ verbose: bool = typer.Option(default=None),
54
+ verbosity: int = typer.Option(
88
55
  None,
89
- "--out", "-o", "--output",
90
- help="Output folder for the code review report"
91
- )
92
-
56
+ '--verbosity', '-v',
57
+ help="Set verbosity level (0-3, default 1)"
58
+ ),
59
+ ):
60
+ if verbose is not None and verbosity is not None:
61
+ raise typer.BadParameter(
62
+ "Please specify either --verbose or --verbosity, not both."
63
+ )
64
+ if verbose is not None:
65
+ verbosity = 2 if verbose else 0
66
+ if verbosity is None:
67
+ verbosity = 1
93
68
 
94
- def arg_against() -> typer.Option:
95
- return typer.Option(
96
- None,
97
- "--against", "-vs", "--vs",
98
- help="Git ref to compare against"
99
- )
69
+ if ctx.invoked_subcommand != "setup":
70
+ bootstrap(verbosity)
100
71
 
101
72
 
102
73
  @app_no_subcommand.command(name="review", help="Perform code review")
@@ -108,16 +79,43 @@ def cmd_review(
108
79
  against: str = arg_against(),
109
80
  filters: str = arg_filters(),
110
81
  merge_base: bool = typer.Option(default=True, help="Use merge base for comparison"),
82
+ url: str = typer.Option("", "--url", help="Git repository URL"),
83
+ post_comment: bool = typer.Option(default=False, help="Post review comment to GitHub"),
84
+ pr: int = typer.Option(
85
+ default=None,
86
+ help=textwrap.dedent("""\n
87
+ GitHub Pull Request number to post the comment to
88
+ (for local usage together with --post-comment,
89
+ in the github actions PR is resolved from the environment)
90
+ """)
91
+ ),
111
92
  out: str = arg_out()
112
93
  ):
113
94
  _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
- ))
95
+ with get_repo_context(url, _what) as (repo, out_folder):
96
+ asyncio.run(review(
97
+ repo=repo,
98
+ what=_what,
99
+ against=_against,
100
+ filters=filters,
101
+ use_merge_base=merge_base,
102
+ out_folder=out or out_folder,
103
+ ))
104
+ if post_comment:
105
+ try:
106
+ owner, repo_name = extract_gh_owner_repo(repo)
107
+ except ValueError as e:
108
+ logging.error(
109
+ "Error posting comment:\n"
110
+ "Could not extract GitHub owner and repository name from the local repository."
111
+ )
112
+ raise typer.Exit(code=1) from e
113
+ post_github_cr_comment(
114
+ md_report_file=os.path.join(out or out_folder, GITHUB_MD_REPORT_FILE_NAME),
115
+ pr=pr,
116
+ gh_repo=f"{owner}/{repo_name}",
117
+ token=resolve_gh_token()
118
+ )
121
119
 
122
120
 
123
121
  @app.command(name="ask", help="Answer questions about codebase changes")
@@ -130,15 +128,32 @@ def cmd_answer(
130
128
  against: str = arg_against(),
131
129
  filters: str = arg_filters(),
132
130
  merge_base: bool = typer.Option(default=True, help="Use merge base for comparison"),
131
+ use_pipeline: bool = typer.Option(default=True),
132
+ post_to: str = typer.Option(
133
+ help="Post answer to ... Supported values: linear",
134
+ default=None,
135
+ show_default=False
136
+ ),
133
137
  ):
134
138
  _what, _against = args_to_target(refs, what, against)
135
- return answer(
139
+ if str(question).startswith("tpl:"):
140
+ prompt_file = str(question)[4:]
141
+ question = ""
142
+ else:
143
+ prompt_file = None
144
+ out = answer(
136
145
  question=question,
137
146
  what=_what,
138
147
  against=_against,
139
148
  filters=filters,
140
149
  use_merge_base=merge_base,
150
+ prompt_file=prompt_file,
151
+ use_pipeline=use_pipeline,
141
152
  )
153
+ if post_to == 'linear':
154
+ logging.info("Posting answer to Linear...")
155
+ linear_comment(remove_html_comments(out))
156
+ return out
142
157
 
143
158
 
144
159
  @app.command(help="Configure LLM for local usage interactively")
@@ -160,31 +175,6 @@ def render(
160
175
  Report.load(file_name=source).to_cli(report_format=format)
161
176
 
162
177
 
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
178
  @app.command(help="List files in the diff. Might be useful to check what will be reviewed.")
189
179
  def files(
190
180
  refs: str = arg_refs(),
@@ -196,22 +186,25 @@ def files(
196
186
  ):
197
187
  _what, _against = args_to_target(refs, what, against)
198
188
  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), " ")))
189
+ try:
190
+ patch_set = get_diff(repo=repo, what=_what, against=_against, use_merge_base=merge_base)
191
+ patch_set = filter_diff(patch_set, filters)
192
+ print(
193
+ f"Changed files: "
194
+ f"{mc.ui.green(_what or 'INDEX')} vs "
195
+ f"{mc.ui.yellow(_against or repo.remotes.origin.refs.HEAD.reference.name)}"
196
+ f"{' filtered by ' + mc.ui.cyan(filters) if filters else ''}"
197
+ )
198
+
199
+ for patch in patch_set:
200
+ if patch.is_added_file:
201
+ color = mc.ui.green
202
+ elif patch.is_removed_file:
203
+ color = mc.ui.red
204
+ else:
205
+ color = mc.ui.blue
206
+ print(f"- {color(patch.path)}")
207
+ if diff:
208
+ print(mc.ui.gray(textwrap.indent(str(patch), " ")))
209
+ finally:
210
+ repo.close()
gito/cli_base.py ADDED
@@ -0,0 +1,90 @@
1
+ import contextlib
2
+ import logging
3
+ import tempfile
4
+
5
+ import microcore as mc
6
+ import typer
7
+ from git import Repo
8
+ from gito.utils import parse_refs_pair
9
+
10
+
11
+ def args_to_target(refs, what, against) -> tuple[str | None, str | None]:
12
+ _what, _against = parse_refs_pair(refs)
13
+ if _what:
14
+ if what:
15
+ raise typer.BadParameter(
16
+ "You cannot specify both 'refs' <WHAT>..<AGAINST> and '--what'. Use one of them."
17
+ )
18
+ else:
19
+ _what = what
20
+ if _against:
21
+ if against:
22
+ raise typer.BadParameter(
23
+ "You cannot specify both 'refs' <WHAT>..<AGAINST> and '--against'. Use one of them."
24
+ )
25
+ else:
26
+ _against = against
27
+ return _what, _against
28
+
29
+
30
+ def arg_refs() -> typer.Argument:
31
+ return typer.Argument(
32
+ default=None,
33
+ help="Git refs to review, [what]..[against] e.g. 'HEAD..HEAD~1'"
34
+ )
35
+
36
+
37
+ def arg_what() -> typer.Option:
38
+ return typer.Option(None, "--what", "-w", help="Git ref to review")
39
+
40
+
41
+ def arg_filters() -> typer.Option:
42
+ return typer.Option(
43
+ "", "--filter", "-f", "--filters",
44
+ help="""
45
+ filter reviewed files by glob / fnmatch pattern(s),
46
+ e.g. 'src/**/*.py', may be comma-separated
47
+ """,
48
+ )
49
+
50
+
51
+ def arg_out() -> typer.Option:
52
+ return typer.Option(
53
+ None,
54
+ "--out", "-o", "--output",
55
+ help="Output folder for the code review report"
56
+ )
57
+
58
+
59
+ def arg_against() -> typer.Option:
60
+ return typer.Option(
61
+ None,
62
+ "--against", "-vs", "--vs",
63
+ help="Git ref to compare against"
64
+ )
65
+
66
+
67
+ app = typer.Typer(pretty_exceptions_show_locals=False)
68
+
69
+
70
+ @contextlib.contextmanager
71
+ def get_repo_context(url: str, branch: str):
72
+ """Context manager for handling both local and remote repositories."""
73
+ if url:
74
+ with tempfile.TemporaryDirectory() as temp_dir:
75
+ logging.info(
76
+ f"get_repo_context: "
77
+ f"Cloning [{mc.ui.green(url)}] to {mc.utils.file_link(temp_dir)} ..."
78
+ )
79
+ repo = Repo.clone_from(url, branch=branch, to_path=temp_dir)
80
+ try:
81
+ yield repo, temp_dir
82
+ finally:
83
+ repo.close()
84
+ else:
85
+ logging.info("get_repo_context: Using local repo...")
86
+ repo = Repo(".")
87
+ try:
88
+ yield repo, "."
89
+ finally:
90
+ repo.close()
@@ -0,0 +1,101 @@
1
+ from pathlib import Path
2
+
3
+ import microcore as mc
4
+ from microcore import ApiType, ui, utils
5
+ from git import Repo
6
+
7
+ from ..utils import version, extract_gh_owner_repo
8
+ from ..cli_base import app
9
+
10
+
11
+ @app.command(name="deploy", help="Deploy Gito workflows to GitHub Actions")
12
+ @app.command(name="init", hidden=True)
13
+ def deploy(api_type: ApiType = None, commit: bool = None, rewrite: bool = False):
14
+ repo = Repo(".")
15
+ workflow_files = dict(
16
+ code_review=Path(".github/workflows/gito-code-review.yml"),
17
+ react_to_comments=Path(".github/workflows/gito-react-to-comments.yml")
18
+ )
19
+ for file in workflow_files.values():
20
+ if file.exists():
21
+ message = f"Gito workflow already exists at {utils.file_link(file)}."
22
+ if rewrite:
23
+ ui.warning(message)
24
+ else:
25
+ message += "\nUse --rewrite to overwrite it."
26
+ ui.error(message)
27
+ return False
28
+
29
+ api_types = [ApiType.ANTHROPIC, ApiType.OPEN_AI, ApiType.GOOGLE_AI_STUDIO]
30
+ default_models = {
31
+ ApiType.ANTHROPIC: "claude-sonnet-4-20250514",
32
+ ApiType.OPEN_AI: "gpt-4.1",
33
+ ApiType.GOOGLE_AI_STUDIO: "gemini-2.5-pro",
34
+ }
35
+ secret_names = {
36
+ ApiType.ANTHROPIC: "ANTHROPIC_API_KEY",
37
+ ApiType.OPEN_AI: "OPENAI_API_KEY",
38
+ ApiType.GOOGLE_AI_STUDIO: "GOOGLE_AI_API_KEY",
39
+ }
40
+ if not api_type:
41
+ api_type = mc.ui.ask_choose(
42
+ "Choose your LLM API type",
43
+ api_types,
44
+ )
45
+ elif api_type not in api_types:
46
+ mc.ui.error(f"Unsupported API type: {api_type}")
47
+ return False
48
+ major, minor, *_ = version().split(".")
49
+ template_vars = dict(
50
+ model=default_models[api_type],
51
+ api_type=api_type,
52
+ secret_name=secret_names[api_type],
53
+ major=major,
54
+ minor=minor,
55
+ ApiType=ApiType,
56
+ remove_indent=True,
57
+ )
58
+ gito_code_review_yml = mc.tpl(
59
+ "github_workflows/gito-code-review.yml.j2",
60
+ **template_vars
61
+ )
62
+ gito_react_to_comments_yml = mc.tpl(
63
+ "github_workflows/gito-react-to-comments.yml.j2",
64
+ **template_vars
65
+ )
66
+
67
+ workflow_files["code_review"].parent.mkdir(parents=True, exist_ok=True)
68
+ workflow_files["code_review"].write_text(gito_code_review_yml)
69
+ workflow_files["react_to_comments"].write_text(gito_react_to_comments_yml)
70
+ print(
71
+ mc.ui.green("Gito workflows have been created.\n")
72
+ + f" - {mc.utils.file_link(workflow_files['code_review'])}\n"
73
+ + f" - {mc.utils.file_link(workflow_files['react_to_comments'])}\n"
74
+ )
75
+ owner, repo_name = extract_gh_owner_repo(repo)
76
+ if commit is True or commit is None and mc.ui.ask_yn(
77
+ "Do you want to commit and push created GitHub workflows to a new branch?"
78
+ ):
79
+ repo.git.add([str(file) for file in workflow_files.values()])
80
+ branch_name = "gito_deploy"
81
+ if not repo.active_branch.name.startswith(branch_name):
82
+ repo.git.checkout("-b", branch_name)
83
+ repo.git.commit("-m", "Deploy Gito workflows")
84
+ repo.git.push("origin", branch_name)
85
+ print(f"Changes pushed to {branch_name} branch.")
86
+ print(
87
+ f"Please create a PR from {branch_name} to your main branch and merge it:\n"
88
+ f"https://github.com/{owner}/{repo_name}/compare/gito_deploy?expand=1"
89
+ )
90
+ else:
91
+ print(
92
+ "Now you can commit and push created GitHub workflows to your main repository branch.\n"
93
+ )
94
+
95
+ print(
96
+ "(!IMPORTANT):\n"
97
+ f"Add {mc.ui.cyan(secret_names[api_type])} with actual API_KEY "
98
+ "to your repository secrets here:\n"
99
+ f"https://github.com/{owner}/{repo_name}/settings/secrets/actions"
100
+ )
101
+ return True
gito/commands/fix.py CHANGED
@@ -10,7 +10,7 @@ import git
10
10
  import typer
11
11
  from microcore import ui
12
12
 
13
- from ..bootstrap import app
13
+ from ..cli_base import app
14
14
  from ..constants import JSON_REPORT_FILE_NAME
15
15
  from ..report_struct import Report, Issue
16
16