ai-cr 1.0.1__py3-none-any.whl → 2.0.0.dev2__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.
@@ -0,0 +1,207 @@
1
+ Metadata-Version: 2.3
2
+ Name: ai-cr
3
+ Version: 2.0.0.dev2
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
+ License: MIT
6
+ Keywords: static code analysis,code review,code quality,ai,coding,assistant,llm,github,automation,devops,developer tools,github actions,workflows,git
7
+ Author: Nayjest
8
+ Author-email: mail@vitaliy.in
9
+ Requires-Python: >=3.11,<4.0
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Software Development
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)
21
+ Requires-Dist: ghapi (>=1.0.6,<1.1.0)
22
+ Requires-Dist: google-generativeai (>=0.8.5,<0.9.0)
23
+ Requires-Dist: jira (>=3.8.0,<4.0.0)
24
+ Requires-Dist: typer (>=0.16.0,<0.17.0)
25
+ Requires-Dist: unidiff (>=0.7.5,<0.8.0)
26
+ Project-URL: Homepage, https://github.com/Nayjest/Gito
27
+ Project-URL: Repository, https://github.com/Nayjest/Gito
28
+ Description-Content-Type: text/markdown
29
+
30
+ <h1 align="center"><a href="#"><img alt="Gito: AI Code Reviewer" src="press-kit/logo/gito-ai-code-reviewer_logo-180.png" align="center" width="180"></a></h1>
31
+ <p align="center">
32
+ <a href="https://pypi.org/project/gito.bot/" target="_blank"><img src="https://img.shields.io/pypi/v/gito.bot" alt="PYPI Release"></a>
33
+ <a href="https://github.com/Nayjest/Gito/actions/workflows/code-style.yml" target="_blank"><img src="https://github.com/Nayjest/Gito/actions/workflows/code-style.yml/badge.svg" alt="PyLint"></a>
34
+ <a href="https://github.com/Nayjest/Gito/actions/workflows/tests.yml" target="_blank"><img src="https://github.com/Nayjest/Gito/actions/workflows/tests.yml/badge.svg" alt="Tests"></a>
35
+ <img src="https://github.com/Nayjest/Gito/blob/main/coverage.svg" alt="Code Coverage">
36
+ <a href="https://github.com/Nayjest/Gito/blob/main/LICENSE" target="_blank"><img src="https://img.shields.io/static/v1?label=license&message=MIT&color=d08aff" alt="License"></a>
37
+ </p>
38
+
39
+ **Gito** is an open-source **AI code reviewer** that works with any language model provider.
40
+ It detects issues in GitHub pull requests or local changes—instantly, reliably, and without vendor lock-in.
41
+
42
+ Get consistent, thorough code reviews in seconds—no waiting for human availability.
43
+
44
+ ## ✨ Why Gito?
45
+
46
+ - [⚡] **Lightning Fast:** Get detailed code reviews in seconds, not days — powered by parallelized LLM processing
47
+ - [🔧] **Vendor Agnostic:** Works with any language model provider (OpenAI, Anthropic, Google, local models, etc.)
48
+ - [🌐] **Universal:** Supports all major programming languages and frameworks
49
+ - [🔍] **Comprehensive Analysis:** Detect issues across security, performance, maintainability, best practices, and much more
50
+ - [📈] **Consistent Quality:** Never tired, never biased—consistent review quality every time
51
+ - [🚀] **Easy Integration:** Automatically reviews pull requests via GitHub Actions and posts results as PR comments
52
+ - [🎛️] **Infinitely Flexible:** Adapt to any project's standards—configure review rules, severity levels, and focus areas, build custom workflows
53
+
54
+ ## 🎯 Perfect For
55
+
56
+ - Solo developers who want expert-level code review without the wait
57
+ - Teams looking to catch issues before human review
58
+ - Open source projects maintaining high code quality at scale
59
+ - CI/CD pipelines requiring automated quality gates
60
+
61
+ ✨ See [code review in action](https://github.com/Nayjest/Gito/pull/39#issuecomment-2906968729) ✨
62
+
63
+ ## 🚀 Quickstart
64
+
65
+ ### 1. Review Pull Requests via GitHub Actions
66
+
67
+ Create a `.github/workflows/gito-code-review.yml` file:
68
+
69
+ ```yaml
70
+ name: "Gito: AI Code Review"
71
+ on: { pull_request: { types: [opened, synchronize, reopened] } }
72
+ jobs:
73
+ review:
74
+ runs-on: ubuntu-latest
75
+ permissions: { contents: read, pull-requests: write } # 'write' for leaving the summary comment
76
+ steps:
77
+ - uses: actions/checkout@v4
78
+ with: { fetch-depth: 0 }
79
+ - name: Set up Python
80
+ uses: actions/setup-python@v5
81
+ with: { python-version: "3.13" }
82
+ - name: Install AI Code Review tool
83
+ run: pip install gito.bot~=2.0
84
+ - name: Run AI code analysis
85
+ env:
86
+ LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
87
+ LLM_API_TYPE: openai
88
+ MODEL: "gpt-4.1"
89
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
90
+ run: |
91
+ gito --verbose review
92
+ gito github-comment --token ${{ secrets.GITHUB_TOKEN }}
93
+ - uses: actions/upload-artifact@v4
94
+ with:
95
+ name: ai-code-review-results
96
+ path: |
97
+ code-review-report.md
98
+ code-review-report.json
99
+ ```
100
+
101
+ > ⚠️ Make sure to add `LLM_API_KEY` to your repository’s GitHub secrets.
102
+
103
+ 💪 Done!
104
+ PRs to your repository will now receive AI code reviews automatically. ✨
105
+ See [GitHub Setup Guide](https://github.com/Nayjest/Gito/blob/main/documentation/github_setup.md) for more details.
106
+
107
+ ### 2. Running Code Analysis Locally
108
+
109
+ #### Initial Local Setup
110
+
111
+ **Prerequisites:** [Python](https://www.python.org/downloads/) 3.11 / 3.12 / 3.13
112
+
113
+ **Step1:** Install [gito.bot](https://github.com/Nayjest/Gito) using [pip](https://en.wikipedia.org/wiki/Pip_(package_manager)).
114
+ ```bash
115
+ pip install gito.bot
116
+ ```
117
+
118
+ > **Troubleshooting:**
119
+ > pip may be also available via cli as `pip3` depending on your Python installation.
120
+
121
+ **Step2:** Perform initial setup
122
+
123
+ The following command will perform one-time setup using an interactive wizard.
124
+ You will be prompted to enter LLM configuration details (API type, API key, etc).
125
+ Configuration will be saved to `~/.gito/.env`.
126
+
127
+ ```bash
128
+ gito setup
129
+ ```
130
+
131
+ > **Troubleshooting:**
132
+ > On some systems, `gito` command may not became available immediately after installation.
133
+ > Try restarting your terminal or running `python -m gito` instead.
134
+
135
+
136
+ #### Perform your first AI code review locally
137
+
138
+ **Step1:** Navigate to your repository root directory.
139
+ **Step2:** Switch to the branch you want to review.
140
+ **Step3:** Run following command
141
+ ```bash
142
+ gito review
143
+ ```
144
+
145
+ > **Note:** This will analyze the current branch against the repository main branch by default.
146
+ > Files that are not staged for commit will be ignored.
147
+ > See `gito --help` for more options.
148
+
149
+ **Reviewing remote repository**
150
+
151
+ ```bash
152
+ gito remote git@github.com:owner/repo.git <FEATURE_BRANCH>..<MAIN_BRANCH>
153
+ ```
154
+ Use interactive help for details:
155
+ ```bash
156
+ gito remote --help
157
+ ```
158
+
159
+ ## 🔧 Configuration
160
+
161
+ Change behavior via `.gito/config.toml`:
162
+
163
+ - Prompt templates, filtering and post-processing using Python code snippets
164
+ - Tagging, severity, and confidence settings
165
+ - Custom AI awards for developer brilliance
166
+ - Output customization
167
+
168
+ You can override the default config by placing `.gito/config.toml` in your repo root.
169
+
170
+
171
+ See default configuration [here](https://github.com/Nayjest/Gito/blob/main/gito/config.toml).
172
+
173
+ More details can be found in [📖 Configuration Cookbook](https://github.com/Nayjest/Gito/blob/main/documentation/config_cookbook.md)
174
+
175
+ ## 💻 Development Setup
176
+
177
+ Install dependencies:
178
+
179
+ ```bash
180
+ make install
181
+ ```
182
+
183
+ Format code and check style:
184
+
185
+ ```bash
186
+ make black
187
+ make cs
188
+ ```
189
+
190
+ Run tests:
191
+
192
+ ```bash
193
+ pytest
194
+ ```
195
+
196
+ ## 🤝 Contributing
197
+
198
+ **Looking for a specific feature or having trouble?**
199
+ Contributions are welcome! ❤️
200
+ See [CONTRIBUTING.md](https://github.com/Nayjest/Gito/blob/main/CONTRIBUTING.md) for details.
201
+
202
+ ## 📝 License
203
+
204
+ Licensed under the [MIT License](https://github.com/Nayjest/Gito/blob/main/LICENSE).
205
+
206
+ © 2025 [Vitalii Stepanenko](mailto:mail@vitaliy.in)
207
+
@@ -0,0 +1,23 @@
1
+ gito/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ gito/__main__.py,sha256=EClCwCzb6h6YBpt0hrnG4h0mlNhNePyg_xBNNSVm1os,65
3
+ gito/bootstrap.py,sha256=ETKioiDc2Npc7znd8HJxA5-twd7sZMPCufIGwXFQSbY,2403
4
+ gito/cli.py,sha256=98Cym9Oil1y1X4SB8cb7xAcJENJPv9ZBPxBEcniB1q4,7994
5
+ gito/commands/__init__.py,sha256=NKUUDskR6taZGbHk5qR9msDS22aHpdzAZlUDxzRdMYA,56
6
+ gito/commands/fix.py,sha256=Nht_14as5txOBGgzrD_1p9pKDJ5J8ghvmaUEVJsJyqI,5462
7
+ gito/commands/gh_comment.py,sha256=uU3CdqsduD_yRfm5eSBC-nE4naofppTkd5inSE0_O_Q,5188
8
+ gito/commands/repl.py,sha256=waN7FJBl98gWmDwZWMa8x157iWbPHIDPJEKmTdzWQ70,396
9
+ gito/config.toml,sha256=NU_nFJtDZVz6dLsz3y33FPiDtf7WP9ZEnzN3mSlxi0s,16002
10
+ gito/constants.py,sha256=DNqh3hOxeLM_KscUH4uFdAc9295o1hiXtwoEw2KxLiU,392
11
+ gito/core.py,sha256=XzrgkNu1GZsjQLkK_PRzV5x-qgdMbhTpSIVrYVadJxc,8300
12
+ gito/issue_trackers.py,sha256=1S8yMuKM6Vq7l81nElNS6IOwXTjEcC5123cp2-6aHkc,453
13
+ gito/pipeline.py,sha256=zI8iRsb8Y18EO6lrUnPFwxBhefJc_VUDVAfcgA9zCWo,2334
14
+ gito/pipeline_steps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ gito/pipeline_steps/jira.py,sha256=hujeWGFAbIHPOidPWgvWPYlgB2hjdXHqU5-lo2Qcjvs,2672
16
+ gito/project_config.py,sha256=LCYMcxXYbiijOMwsDwr4FY_E4pDHwUE7UkRzJkgX1Uk,2568
17
+ gito/report_struct.py,sha256=xX93WJSAzVP5_w4fFXjeuDXEOGPvTsEEvLqcOPfWPXM,4234
18
+ gito/utils.py,sha256=BXy7wT7S9CNymPyNlgFqH8WXNTfErKQ6WQlM-IfqYGg,6489
19
+ ai_cr-2.0.0.dev2.dist-info/entry_points.txt,sha256=Ua1DxkhJJ8TZuLgnH-IlWCkrre_0S0dq_GtYRaYupWk,38
20
+ ai_cr-2.0.0.dev2.dist-info/LICENSE,sha256=XATf3zv-CppUSJqI18KLhwnPEomUXEl5WbBzFyb9OSU,1096
21
+ ai_cr-2.0.0.dev2.dist-info/METADATA,sha256=TRMBhhnhRBWsMGbHZsyfXG0-VFchXmmw_HwTjd8YnHk,7763
22
+ ai_cr-2.0.0.dev2.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
23
+ ai_cr-2.0.0.dev2.dist-info/RECORD,,
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ gito=gito.cli:main
3
+
@@ -3,9 +3,10 @@ import os
3
3
  from datetime import datetime
4
4
 
5
5
  import microcore as mc
6
- from ai_code_review.utils import is_running_in_github_action
6
+ import typer
7
7
 
8
- from .constants import ENV_CONFIG_FILE
8
+ from .utils import is_running_in_github_action
9
+ from .constants import HOME_ENV_PATH, EXECUTABLE
9
10
 
10
11
 
11
12
  def setup_logging():
@@ -32,7 +33,7 @@ def bootstrap():
32
33
  logging.info("Bootstrapping...")
33
34
  try:
34
35
  mc.configure(
35
- DOT_ENV_FILE=ENV_CONFIG_FILE,
36
+ DOT_ENV_FILE=HOME_ENV_PATH,
36
37
  USE_LOGGING=True,
37
38
  EMBEDDING_DB_TYPE=mc.EmbeddingDbType.NONE,
38
39
  )
@@ -51,7 +52,7 @@ def bootstrap():
51
52
  )
52
53
  else:
53
54
  msg += (
54
- "\nPlease run 'ai-code-review setup' "
55
+ f"\nPlease run '{EXECUTABLE} setup' "
55
56
  "to configure LLM API access (API keys, model, etc)."
56
57
  )
57
58
  print(mc.ui.red(msg))
@@ -60,3 +61,6 @@ def bootstrap():
60
61
  logging.error(f"Unexpected configuration error: {e}")
61
62
  raise SystemExit(3)
62
63
  mc.logging.LoggingConfig.STRIP_REQUEST_LINES = [300, 15]
64
+
65
+
66
+ app = typer.Typer(pretty_exceptions_show_locals=False)
@@ -12,18 +12,25 @@ from git import Repo
12
12
 
13
13
  from .core import review, get_diff, filter_diff
14
14
  from .report_struct import Report
15
- from .constants import ENV_CONFIG_FILE
16
- from .bootstrap import bootstrap
15
+ from .constants import HOME_ENV_PATH
16
+ from .bootstrap import bootstrap, app
17
17
  from .project_config import ProjectConfig
18
18
  from .utils import no_subcommand, parse_refs_pair
19
19
 
20
- app = typer.Typer(pretty_exceptions_show_locals=False)
20
+ # Import fix command to register it
21
+ from .commands import fix, gh_comment # noqa
22
+
23
+
21
24
  app_no_subcommand = typer.Typer(pretty_exceptions_show_locals=False)
22
25
 
23
26
 
24
27
  def main():
25
28
  if sys.platform == "win32":
26
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
+
27
34
  if no_subcommand(app):
28
35
  bootstrap()
29
36
  app_no_subcommand()
@@ -33,10 +40,10 @@ def main():
33
40
 
34
41
  @app.callback(invoke_without_command=True)
35
42
  def cli(ctx: typer.Context, verbose: bool = typer.Option(default=False)):
36
- if verbose:
37
- mc.logging.LoggingConfig.STRIP_REQUEST_LINES = None
38
43
  if ctx.invoked_subcommand != "setup":
39
44
  bootstrap()
45
+ if verbose:
46
+ mc.logging.LoggingConfig.STRIP_REQUEST_LINES = None
40
47
 
41
48
 
42
49
  def args_to_target(refs, what, against) -> tuple[str | None, str | None]:
@@ -97,6 +104,7 @@ def arg_against() -> typer.Option:
97
104
 
98
105
  @app_no_subcommand.command(name="review", help="Perform code review")
99
106
  @app.command(name="review", help="Perform code review")
107
+ @app.command(name="run", hidden=True)
100
108
  def cmd_review(
101
109
  refs: str = arg_refs(),
102
110
  what: str = arg_what(),
@@ -117,12 +125,21 @@ def cmd_review(
117
125
 
118
126
  @app.command(help="Configure LLM for local usage interactively")
119
127
  def setup():
120
- mc.interactive_setup(ENV_CONFIG_FILE)
121
-
122
-
123
- @app.command()
124
- def render(format: str = Report.Format.MARKDOWN):
125
- print(Report.load().render(format=format))
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)
126
143
 
127
144
 
128
145
  @app.command(help="Review remote code")
@@ -225,7 +242,7 @@ def files(
225
242
  f"{mc.ui.yellow(_against or repo.remotes.origin.refs.HEAD.reference.name)}"
226
243
  f"{' filtered by '+mc.ui.cyan(filters) if filters else ''}"
227
244
  )
228
-
245
+ repo.close()
229
246
  for patch in patch_set:
230
247
  if patch.is_added_file:
231
248
  color = mc.ui.green
@@ -0,0 +1 @@
1
+ # Command modules register themselves with the CLI app
gito/commands/fix.py ADDED
@@ -0,0 +1,157 @@
1
+ """
2
+ Fix issues from code review report
3
+ """
4
+ import json
5
+ import logging
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import git
10
+ import typer
11
+ from microcore import ui
12
+
13
+ from ..bootstrap import app
14
+ from ..constants import JSON_REPORT_FILE_NAME
15
+ from ..report_struct import Report, Issue
16
+
17
+
18
+ @app.command(help="Fix an issue from the code review report")
19
+ def fix(
20
+ issue_number: int = typer.Argument(..., help="Issue number to fix"),
21
+ report_path: Optional[str] = typer.Option(
22
+ None,
23
+ "--report",
24
+ "-r",
25
+ help="Path to the code review report (default: code-review-report.json)"
26
+ ),
27
+ dry_run: bool = typer.Option(
28
+ False, "--dry-run", "-d", help="Only print changes without applying them"
29
+ ),
30
+ commit: bool = typer.Option(default=False, help="Commit changes after applying them"),
31
+ push: bool = typer.Option(default=False, help="Push changes to the remote repository"),
32
+ ) -> list[str]:
33
+ """
34
+ Applies the proposed change for the specified issue number from the code review report.
35
+ """
36
+ # Load the report
37
+ report_path = report_path or JSON_REPORT_FILE_NAME
38
+ try:
39
+ report = Report.load(report_path)
40
+ except (FileNotFoundError, json.JSONDecodeError) as e:
41
+ logging.error(f"Failed to load report from {report_path}: {e}")
42
+ raise typer.Exit(code=1)
43
+
44
+ # Find the issue by number
45
+ issue: Optional[Issue] = None
46
+ for file_issues in report.issues.values():
47
+ for i in file_issues:
48
+ if i.id == issue_number:
49
+ issue = i
50
+ break
51
+ if issue:
52
+ break
53
+
54
+ if not issue:
55
+ logging.error(f"Issue #{issue_number} not found in the report")
56
+ raise typer.Exit(code=1)
57
+
58
+ if not issue.affected_lines:
59
+ logging.error(f"Issue #{issue_number} has no affected lines specified")
60
+ raise typer.Exit(code=1)
61
+
62
+ if not any(affected_line.proposal for affected_line in issue.affected_lines):
63
+ logging.error(f"Issue #{issue_number} has no proposal for fixing")
64
+ raise typer.Exit(code=1)
65
+
66
+ # Apply the fix
67
+ logging.info(f"Fixing issue #{issue_number}: {ui.cyan(issue.title)}")
68
+
69
+ for affected_line in issue.affected_lines:
70
+ if not affected_line.proposal:
71
+ continue
72
+
73
+ file_path = Path(issue.file)
74
+ if not file_path.exists():
75
+ logging.error(f"File {file_path} not found")
76
+ continue
77
+
78
+ try:
79
+ with open(file_path, "r", encoding="utf-8") as f:
80
+ lines = f.readlines()
81
+ except Exception as e:
82
+ logging.error(f"Failed to read file {file_path}: {e}")
83
+ continue
84
+
85
+ # Check if line numbers are valid
86
+ if affected_line.start_line < 1 or affected_line.end_line > len(lines):
87
+ logging.error(
88
+ f"Invalid line range: {affected_line.start_line}-{affected_line.end_line} "
89
+ f"(file has {len(lines)} lines)"
90
+ )
91
+ continue
92
+
93
+ # Get the affected line content for display
94
+ affected_content = "".join(lines[affected_line.start_line - 1:affected_line.end_line])
95
+ print(f"\nFile: {ui.blue(issue.file)}")
96
+ print(f"Lines: {affected_line.start_line}-{affected_line.end_line}")
97
+ print(f"Current content:\n{ui.red(affected_content)}")
98
+ print(f"Proposed change:\n{ui.green(affected_line.proposal)}")
99
+
100
+ if dry_run:
101
+ print(f"{ui.yellow('Dry run')}: Changes not applied")
102
+ continue
103
+
104
+ # Apply the change
105
+ proposal_lines = affected_line.proposal.splitlines(keepends=True)
106
+ if not proposal_lines:
107
+ proposal_lines = [""]
108
+ elif not proposal_lines[-1].endswith(("\n", "\r")):
109
+ # Ensure the last line has a newline if the original does
110
+ if (
111
+ affected_line.end_line < len(lines)
112
+ and lines[affected_line.end_line - 1].endswith(("\n", "\r"))
113
+ ):
114
+ proposal_lines[-1] += "\n"
115
+
116
+ lines[affected_line.start_line - 1:affected_line.end_line] = proposal_lines
117
+
118
+ # Write changes back to the file
119
+ try:
120
+ with open(file_path, "w", encoding="utf-8") as f:
121
+ f.writelines(lines)
122
+ print(f"{ui.green('Success')}: Changes applied to {file_path}")
123
+ except Exception as e:
124
+ logging.error(f"Failed to write changes to {file_path}: {e}")
125
+ raise typer.Exit(code=1)
126
+
127
+ print(f"\n{ui.green('✓')} Issue #{issue_number} fixed successfully")
128
+
129
+ changed_files = [file_path.as_posix()]
130
+ if commit:
131
+ commit_changes(
132
+ changed_files,
133
+ commit_message=f"[AI] Fix issue {issue_number}:{issue.title}",
134
+ push=push
135
+ )
136
+ return changed_files
137
+
138
+
139
+ def commit_changes(
140
+ files: list[str],
141
+ repo: git.Repo = None,
142
+ commit_message: str = "fix by AI",
143
+ push: bool = True
144
+ ) -> None:
145
+ if opened_repo := not repo:
146
+ repo = git.Repo(".")
147
+ for i in files:
148
+ repo.index.add(i)
149
+ repo.index.commit(commit_message)
150
+ if push:
151
+ origin = repo.remotes.origin
152
+ origin.push()
153
+ logging.info(f"Changes pushed to {origin.name}")
154
+ else:
155
+ logging.info("Changes committed but not pushed to remote")
156
+ if opened_repo:
157
+ repo.close()
@@ -0,0 +1,157 @@
1
+ """
2
+ Fix issues from code review report
3
+ """
4
+
5
+ import logging
6
+ import os
7
+ import re
8
+ from pathlib import Path
9
+ from typing import Optional
10
+ import zipfile
11
+
12
+ import requests
13
+ import typer
14
+ from fastcore.basics import AttrDict
15
+ from gito.project_config import ProjectConfig
16
+ from gito.utils import extract_gh_owner_repo
17
+ from microcore import ui
18
+ from ghapi.all import GhApi
19
+ import git
20
+
21
+ from ..bootstrap import app
22
+ from ..constants import JSON_REPORT_FILE_NAME
23
+ from .fix import fix
24
+ from ..utils import is_running_in_github_action
25
+
26
+
27
+ @app.command()
28
+ def react_to_comment(
29
+ comment_id: int = typer.Argument(),
30
+ gh_token: str = typer.Option(
31
+ "",
32
+ "--gh-token",
33
+ "--token",
34
+ "-t",
35
+ "--github-token",
36
+ help="GitHub token for authentication",
37
+ ),
38
+ dry_run: bool = typer.Option(
39
+ False, "--dry-run", "-d", help="Only print changes without applying them"
40
+ ),
41
+ ):
42
+ repo = git.Repo(".") # Current directory
43
+ owner, repo_name = extract_gh_owner_repo(repo)
44
+ logging.info(f"Using repository: {ui.yellow}{owner}/{repo_name}{ui.reset}")
45
+ gh_token = (
46
+ gh_token or os.getenv("GITHUB_TOKEN", None) or os.getenv("GH_TOKEN", None)
47
+ )
48
+ api = GhApi(owner=owner, repo=repo_name, token=gh_token)
49
+ comment = api.issues.get_comment(comment_id=comment_id)
50
+ logging.info(
51
+ f"Comment by {ui.yellow('@' + comment.user.login)}: "
52
+ f"{ui.green(comment.body)}\n"
53
+ f"url: {comment.html_url}"
54
+ )
55
+
56
+ cfg = ProjectConfig.load_for_repo(repo)
57
+ if not any(
58
+ trigger.lower() in comment.body.lower() for trigger in cfg.mention_triggers
59
+ ):
60
+ ui.error("No mention trigger found in comment, no reaction added.")
61
+ return
62
+ if not is_running_in_github_action():
63
+ # @todo: need service account to react to comments
64
+ logging.info("Comment contains mention trigger, reacting with 'eyes'.")
65
+ api.reactions.create_for_issue_comment(comment_id=comment_id, content="eyes")
66
+
67
+ pr = int(comment.issue_url.split("/")[-1])
68
+ print(f"Processing comment for PR #{pr}...")
69
+ out_folder = "artifact"
70
+ download_latest_code_review_artifact(
71
+ api, pr_number=pr, gh_token=gh_token, out_folder=out_folder
72
+ )
73
+
74
+ issue_ids = extract_fix_args(comment.body)
75
+ if not issue_ids:
76
+ ui.error("Can't identify target command in the text.")
77
+ return
78
+ logging.info(f"Extracted issue IDs: {ui.yellow(str(issue_ids))}")
79
+
80
+ fix(
81
+ issue_ids[0], # @todo: support multiple IDs
82
+ report_path=Path(out_folder) / JSON_REPORT_FILE_NAME,
83
+ dry_run=dry_run,
84
+ commit=not dry_run,
85
+ push=not dry_run,
86
+ )
87
+ logging.info("Fix applied successfully.")
88
+
89
+
90
+ def last_code_review_run(api: GhApi, pr_number: int) -> AttrDict | None:
91
+ pr = api.pulls.get(pr_number)
92
+ sha = pr["head"]["sha"] # noqa
93
+ branch = pr["head"]["ref"]
94
+
95
+ runs = api.actions.list_workflow_runs_for_repo(branch=branch)["workflow_runs"]
96
+ # Find the run for this SHA
97
+ run = next(
98
+ (
99
+ r
100
+ for r in runs # r['head_sha'] == sha and
101
+ if (
102
+ any(
103
+ marker in r["path"].lower()
104
+ for marker in ["code-review", "code_review", "cr"]
105
+ )
106
+ or "gito.yml" in r["name"].lower()
107
+ )
108
+ and r["status"] == "completed"
109
+ ),
110
+ None,
111
+ )
112
+ return run
113
+
114
+
115
+ def download_latest_code_review_artifact(
116
+ api: GhApi, pr_number: int, gh_token: str, out_folder: Optional[str] = "artifact"
117
+ ) -> tuple[str, dict] | None:
118
+ run = last_code_review_run(api, pr_number)
119
+ if not run:
120
+ raise Exception("No workflow run found for this PR/SHA")
121
+
122
+ artifacts = api.actions.list_workflow_run_artifacts(run["id"])["artifacts"]
123
+ if not artifacts:
124
+ raise Exception("No artifacts found for this workflow run")
125
+
126
+ latest_artifact = artifacts[0]
127
+ url = latest_artifact["archive_download_url"]
128
+ print(f"Artifact: {latest_artifact['name']}, Download URL: {url}")
129
+ headers = {"Authorization": f"token {gh_token}"} if gh_token else {}
130
+ zip_path = "artifact.zip"
131
+ try:
132
+ with requests.get(url, headers=headers, stream=True) as r:
133
+ r.raise_for_status()
134
+ with open(zip_path, "wb") as f:
135
+ for chunk in r.iter_content(chunk_size=8192):
136
+ f.write(chunk)
137
+
138
+ # Unpack to ./artifact
139
+ os.makedirs("artifact", exist_ok=True)
140
+ with zipfile.ZipFile(zip_path, "r") as zip_ref:
141
+ zip_ref.extractall("artifact")
142
+ finally:
143
+ if os.path.exists(zip_path):
144
+ os.remove(zip_path)
145
+
146
+ print("Artifact unpacked to ./artifact")
147
+
148
+
149
+ def extract_fix_args(text: str) -> list[int]:
150
+ pattern1 = r"fix\s+(?:issues?)?(?:\s+)?#?(\d+(?:\s*,\s*#?\d+)*)"
151
+ match = re.search(pattern1, text)
152
+ if match:
153
+ numbers_str = match.group(1)
154
+ numbers = re.findall(r"\d+", numbers_str)
155
+ issue_numbers = [int(num) for num in numbers]
156
+ return issue_numbers
157
+ return []