ai-cr 1.0.0__py3-none-any.whl → 2.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.
- {ai_cr-1.0.0.dist-info → ai_cr-2.0.0.dist-info}/LICENSE +21 -21
- ai_cr-2.0.0.dist-info/METADATA +215 -0
- ai_cr-2.0.0.dist-info/RECORD +26 -0
- {ai_cr-1.0.0.dist-info → ai_cr-2.0.0.dist-info}/WHEEL +1 -1
- ai_cr-2.0.0.dist-info/entry_points.txt +3 -0
- {ai_code_review → gito}/__main__.py +4 -4
- {ai_code_review → gito}/bootstrap.py +66 -62
- {ai_code_review → gito}/cli.py +217 -238
- gito/commands/__init__.py +1 -0
- gito/commands/fix.py +157 -0
- gito/commands/gh_post_review_comment.py +63 -0
- gito/commands/gh_react_to_comment.py +194 -0
- {ai_code_review → gito}/commands/repl.py +7 -2
- ai_code_review/.ai-code-review.toml → gito/config.toml +453 -311
- gito/constants.py +12 -0
- {ai_code_review → gito}/core.py +288 -191
- gito/gh_api.py +35 -0
- gito/issue_trackers.py +49 -0
- gito/pipeline.py +82 -0
- gito/pipeline_steps/__init__.py +0 -0
- gito/pipeline_steps/jira.py +57 -0
- gito/pipeline_steps/linear.py +84 -0
- gito/project_config.py +73 -0
- {ai_code_review → gito}/report_struct.py +134 -108
- gito/utils.py +226 -0
- ai_code_review/constants.py +0 -7
- ai_code_review/project_config.py +0 -99
- ai_code_review/utils.py +0 -116
- ai_cr-1.0.0.dist-info/METADATA +0 -197
- ai_cr-1.0.0.dist-info/RECORD +0 -16
- ai_cr-1.0.0.dist-info/entry_points.txt +0 -3
- {ai_code_review → gito}/__init__.py +0 -0
{ai_code_review → gito}/cli.py
RENAMED
@@ -1,238 +1,217 @@
|
|
1
|
-
import asyncio
|
2
|
-
import logging
|
3
|
-
import sys
|
4
|
-
import
|
5
|
-
import
|
6
|
-
|
7
|
-
import
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
from
|
12
|
-
|
13
|
-
from .
|
14
|
-
from .
|
15
|
-
from .
|
16
|
-
|
17
|
-
|
18
|
-
from .
|
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
|
-
if
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
return
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
def
|
73
|
-
return typer.Option(
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
)
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
@app.command()
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
)
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
)
|
218
|
-
_what, _against = args_to_target(refs, what, against)
|
219
|
-
repo = Repo(".")
|
220
|
-
patch_set = get_diff(repo=repo, what=_what, against=_against, use_merge_base=merge_base)
|
221
|
-
patch_set = filter_diff(patch_set, filters)
|
222
|
-
print(
|
223
|
-
f"Changed files: "
|
224
|
-
f"{mc.ui.green(_what or 'INDEX')} vs "
|
225
|
-
f"{mc.ui.yellow(_against or repo.remotes.origin.refs.HEAD.reference.name)}"
|
226
|
-
f"{' filtered by '+mc.ui.cyan(filters) if filters else ''}"
|
227
|
-
)
|
228
|
-
|
229
|
-
for patch in patch_set:
|
230
|
-
if patch.is_added_file:
|
231
|
-
color = mc.ui.green
|
232
|
-
elif patch.is_removed_file:
|
233
|
-
color = mc.ui.red
|
234
|
-
else:
|
235
|
-
color = mc.ui.blue
|
236
|
-
print(f"- {color(patch.path)}")
|
237
|
-
if diff:
|
238
|
-
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), " ")))
|
@@ -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()
|