ai-cr 3.2.1__py3-none-any.whl → 3.3.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-3.2.1.dist-info → ai_cr-3.3.0.dist-info}/LICENSE +21 -21
- {ai_cr-3.2.1.dist-info → ai_cr-3.3.0.dist-info}/METADATA +1 -1
- ai_cr-3.3.0.dist-info/RECORD +41 -0
- {ai_cr-3.2.1.dist-info → ai_cr-3.3.0.dist-info}/WHEEL +1 -1
- gito/__main__.py +4 -4
- gito/bootstrap.py +90 -90
- gito/cli.py +255 -244
- gito/cli_base.py +104 -94
- gito/commands/__init__.py +1 -1
- gito/commands/deploy.py +138 -138
- gito/commands/fix.py +160 -160
- gito/commands/gh_post_review_comment.py +111 -111
- gito/commands/gh_react_to_comment.py +217 -217
- gito/commands/linear_comment.py +53 -53
- gito/commands/repl.py +30 -30
- gito/commands/version.py +8 -8
- gito/config.toml +450 -448
- gito/constants.py +15 -14
- gito/context.py +19 -19
- gito/core.py +520 -508
- gito/env.py +8 -7
- gito/gh_api.py +116 -116
- gito/issue_trackers.py +50 -50
- gito/pipeline.py +83 -83
- gito/pipeline_steps/jira.py +62 -62
- gito/pipeline_steps/linear.py +85 -85
- gito/project_config.py +85 -85
- gito/report_struct.py +136 -136
- gito/tpl/answer.j2 +25 -25
- gito/tpl/github_workflows/components/env-vars.j2 +11 -11
- gito/tpl/github_workflows/components/installs.j2 +23 -23
- gito/tpl/github_workflows/gito-code-review.yml.j2 +32 -32
- gito/tpl/github_workflows/gito-react-to-comments.yml.j2 +70 -70
- gito/tpl/partial/aux_files.j2 +8 -8
- gito/tpl/questions/changes_summary.j2 +55 -55
- gito/tpl/questions/release_notes.j2 +26 -26
- gito/tpl/questions/test_cases.j2 +37 -37
- gito/utils.py +267 -242
- ai_cr-3.2.1.dist-info/RECORD +0 -41
- {ai_cr-3.2.1.dist-info → ai_cr-3.3.0.dist-info}/entry_points.txt +0 -0
gito/commands/fix.py
CHANGED
@@ -1,160 +1,160 @@
|
|
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 ..cli_base import app
|
14
|
-
from ..constants import JSON_REPORT_FILE_NAME
|
15
|
-
from ..report_struct import Report, Issue
|
16
|
-
|
17
|
-
|
18
|
-
@app.command(
|
19
|
-
help="Fix an issue from the code review report "
|
20
|
-
"(latest code review results will be used by default)"
|
21
|
-
)
|
22
|
-
def fix(
|
23
|
-
issue_number: int = typer.Argument(..., help="Issue number to fix"),
|
24
|
-
report_path: Optional[str] = typer.Option(
|
25
|
-
None,
|
26
|
-
"--report",
|
27
|
-
"-r",
|
28
|
-
help="Path to the code review report (default: code-review-report.json)"
|
29
|
-
),
|
30
|
-
dry_run: bool = typer.Option(
|
31
|
-
False, "--dry-run", "-d", help="Only print changes without applying them"
|
32
|
-
),
|
33
|
-
commit: bool = typer.Option(default=False, help="Commit changes after applying them"),
|
34
|
-
push: bool = typer.Option(default=False, help="Push changes to the remote repository"),
|
35
|
-
) -> list[str]:
|
36
|
-
"""
|
37
|
-
Applies the proposed change for the specified issue number from the code review report.
|
38
|
-
"""
|
39
|
-
# Load the report
|
40
|
-
report_path = report_path or JSON_REPORT_FILE_NAME
|
41
|
-
try:
|
42
|
-
report = Report.load(report_path)
|
43
|
-
except (FileNotFoundError, json.JSONDecodeError) as e:
|
44
|
-
logging.error(f"Failed to load report from {report_path}: {e}")
|
45
|
-
raise typer.Exit(code=1)
|
46
|
-
|
47
|
-
# Find the issue by number
|
48
|
-
issue: Optional[Issue] = None
|
49
|
-
for file_issues in report.issues.values():
|
50
|
-
for i in file_issues:
|
51
|
-
if i.id == issue_number:
|
52
|
-
issue = i
|
53
|
-
break
|
54
|
-
if issue:
|
55
|
-
break
|
56
|
-
|
57
|
-
if not issue:
|
58
|
-
logging.error(f"Issue #{issue_number} not found in the report")
|
59
|
-
raise typer.Exit(code=1)
|
60
|
-
|
61
|
-
if not issue.affected_lines:
|
62
|
-
logging.error(f"Issue #{issue_number} has no affected lines specified")
|
63
|
-
raise typer.Exit(code=1)
|
64
|
-
|
65
|
-
if not any(affected_line.proposal for affected_line in issue.affected_lines):
|
66
|
-
logging.error(f"Issue #{issue_number} has no proposal for fixing")
|
67
|
-
raise typer.Exit(code=1)
|
68
|
-
|
69
|
-
# Apply the fix
|
70
|
-
logging.info(f"Fixing issue #{issue_number}: {ui.cyan(issue.title)}")
|
71
|
-
|
72
|
-
for affected_line in issue.affected_lines:
|
73
|
-
if not affected_line.proposal:
|
74
|
-
continue
|
75
|
-
|
76
|
-
file_path = Path(issue.file)
|
77
|
-
if not file_path.exists():
|
78
|
-
logging.error(f"File {file_path} not found")
|
79
|
-
continue
|
80
|
-
|
81
|
-
try:
|
82
|
-
with open(file_path, "r", encoding="utf-8") as f:
|
83
|
-
lines = f.readlines()
|
84
|
-
except Exception as e:
|
85
|
-
logging.error(f"Failed to read file {file_path}: {e}")
|
86
|
-
continue
|
87
|
-
|
88
|
-
# Check if line numbers are valid
|
89
|
-
if affected_line.start_line < 1 or affected_line.end_line > len(lines):
|
90
|
-
logging.error(
|
91
|
-
f"Invalid line range: {affected_line.start_line}-{affected_line.end_line} "
|
92
|
-
f"(file has {len(lines)} lines)"
|
93
|
-
)
|
94
|
-
continue
|
95
|
-
|
96
|
-
# Get the affected line content for display
|
97
|
-
affected_content = "".join(lines[affected_line.start_line - 1:affected_line.end_line])
|
98
|
-
print(f"\nFile: {ui.blue(issue.file)}")
|
99
|
-
print(f"Lines: {affected_line.start_line}-{affected_line.end_line}")
|
100
|
-
print(f"Current content:\n{ui.red(affected_content)}")
|
101
|
-
print(f"Proposed change:\n{ui.green(affected_line.proposal)}")
|
102
|
-
|
103
|
-
if dry_run:
|
104
|
-
print(f"{ui.yellow('Dry run')}: Changes not applied")
|
105
|
-
continue
|
106
|
-
|
107
|
-
# Apply the change
|
108
|
-
proposal_lines = affected_line.proposal.splitlines(keepends=True)
|
109
|
-
if not proposal_lines:
|
110
|
-
proposal_lines = [""]
|
111
|
-
elif not proposal_lines[-1].endswith(("\n", "\r")):
|
112
|
-
# Ensure the last line has a newline if the original does
|
113
|
-
if (
|
114
|
-
affected_line.end_line < len(lines)
|
115
|
-
and lines[affected_line.end_line - 1].endswith(("\n", "\r"))
|
116
|
-
):
|
117
|
-
proposal_lines[-1] += "\n"
|
118
|
-
|
119
|
-
lines[affected_line.start_line - 1:affected_line.end_line] = proposal_lines
|
120
|
-
|
121
|
-
# Write changes back to the file
|
122
|
-
try:
|
123
|
-
with open(file_path, "w", encoding="utf-8") as f:
|
124
|
-
f.writelines(lines)
|
125
|
-
print(f"{ui.green('Success')}: Changes applied to {file_path}")
|
126
|
-
except Exception as e:
|
127
|
-
logging.error(f"Failed to write changes to {file_path}: {e}")
|
128
|
-
raise typer.Exit(code=1)
|
129
|
-
|
130
|
-
print(f"\n{ui.green('✓')} Issue #{issue_number} fixed successfully")
|
131
|
-
|
132
|
-
changed_files = [file_path.as_posix()]
|
133
|
-
if commit:
|
134
|
-
commit_changes(
|
135
|
-
changed_files,
|
136
|
-
commit_message=f"[AI] Fix issue {issue_number}:{issue.title}",
|
137
|
-
push=push
|
138
|
-
)
|
139
|
-
return changed_files
|
140
|
-
|
141
|
-
|
142
|
-
def commit_changes(
|
143
|
-
files: list[str],
|
144
|
-
repo: git.Repo = None,
|
145
|
-
commit_message: str = "fix by AI",
|
146
|
-
push: bool = True
|
147
|
-
) -> None:
|
148
|
-
if opened_repo := not repo:
|
149
|
-
repo = git.Repo(".")
|
150
|
-
for i in files:
|
151
|
-
repo.index.add(i)
|
152
|
-
repo.index.commit(commit_message)
|
153
|
-
if push:
|
154
|
-
origin = repo.remotes.origin
|
155
|
-
origin.push()
|
156
|
-
logging.info(f"Changes pushed to {origin.name}")
|
157
|
-
else:
|
158
|
-
logging.info("Changes committed but not pushed to remote")
|
159
|
-
if opened_repo:
|
160
|
-
repo.close()
|
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 ..cli_base import app
|
14
|
+
from ..constants import JSON_REPORT_FILE_NAME
|
15
|
+
from ..report_struct import Report, Issue
|
16
|
+
|
17
|
+
|
18
|
+
@app.command(
|
19
|
+
help="Fix an issue from the code review report "
|
20
|
+
"(latest code review results will be used by default)"
|
21
|
+
)
|
22
|
+
def fix(
|
23
|
+
issue_number: int = typer.Argument(..., help="Issue number to fix"),
|
24
|
+
report_path: Optional[str] = typer.Option(
|
25
|
+
None,
|
26
|
+
"--report",
|
27
|
+
"-r",
|
28
|
+
help="Path to the code review report (default: code-review-report.json)"
|
29
|
+
),
|
30
|
+
dry_run: bool = typer.Option(
|
31
|
+
False, "--dry-run", "-d", help="Only print changes without applying them"
|
32
|
+
),
|
33
|
+
commit: bool = typer.Option(default=False, help="Commit changes after applying them"),
|
34
|
+
push: bool = typer.Option(default=False, help="Push changes to the remote repository"),
|
35
|
+
) -> list[str]:
|
36
|
+
"""
|
37
|
+
Applies the proposed change for the specified issue number from the code review report.
|
38
|
+
"""
|
39
|
+
# Load the report
|
40
|
+
report_path = report_path or JSON_REPORT_FILE_NAME
|
41
|
+
try:
|
42
|
+
report = Report.load(report_path)
|
43
|
+
except (FileNotFoundError, json.JSONDecodeError) as e:
|
44
|
+
logging.error(f"Failed to load report from {report_path}: {e}")
|
45
|
+
raise typer.Exit(code=1)
|
46
|
+
|
47
|
+
# Find the issue by number
|
48
|
+
issue: Optional[Issue] = None
|
49
|
+
for file_issues in report.issues.values():
|
50
|
+
for i in file_issues:
|
51
|
+
if i.id == issue_number:
|
52
|
+
issue = i
|
53
|
+
break
|
54
|
+
if issue:
|
55
|
+
break
|
56
|
+
|
57
|
+
if not issue:
|
58
|
+
logging.error(f"Issue #{issue_number} not found in the report")
|
59
|
+
raise typer.Exit(code=1)
|
60
|
+
|
61
|
+
if not issue.affected_lines:
|
62
|
+
logging.error(f"Issue #{issue_number} has no affected lines specified")
|
63
|
+
raise typer.Exit(code=1)
|
64
|
+
|
65
|
+
if not any(affected_line.proposal for affected_line in issue.affected_lines):
|
66
|
+
logging.error(f"Issue #{issue_number} has no proposal for fixing")
|
67
|
+
raise typer.Exit(code=1)
|
68
|
+
|
69
|
+
# Apply the fix
|
70
|
+
logging.info(f"Fixing issue #{issue_number}: {ui.cyan(issue.title)}")
|
71
|
+
|
72
|
+
for affected_line in issue.affected_lines:
|
73
|
+
if not affected_line.proposal:
|
74
|
+
continue
|
75
|
+
|
76
|
+
file_path = Path(issue.file)
|
77
|
+
if not file_path.exists():
|
78
|
+
logging.error(f"File {file_path} not found")
|
79
|
+
continue
|
80
|
+
|
81
|
+
try:
|
82
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
83
|
+
lines = f.readlines()
|
84
|
+
except Exception as e:
|
85
|
+
logging.error(f"Failed to read file {file_path}: {e}")
|
86
|
+
continue
|
87
|
+
|
88
|
+
# Check if line numbers are valid
|
89
|
+
if affected_line.start_line < 1 or affected_line.end_line > len(lines):
|
90
|
+
logging.error(
|
91
|
+
f"Invalid line range: {affected_line.start_line}-{affected_line.end_line} "
|
92
|
+
f"(file has {len(lines)} lines)"
|
93
|
+
)
|
94
|
+
continue
|
95
|
+
|
96
|
+
# Get the affected line content for display
|
97
|
+
affected_content = "".join(lines[affected_line.start_line - 1:affected_line.end_line])
|
98
|
+
print(f"\nFile: {ui.blue(issue.file)}")
|
99
|
+
print(f"Lines: {affected_line.start_line}-{affected_line.end_line}")
|
100
|
+
print(f"Current content:\n{ui.red(affected_content)}")
|
101
|
+
print(f"Proposed change:\n{ui.green(affected_line.proposal)}")
|
102
|
+
|
103
|
+
if dry_run:
|
104
|
+
print(f"{ui.yellow('Dry run')}: Changes not applied")
|
105
|
+
continue
|
106
|
+
|
107
|
+
# Apply the change
|
108
|
+
proposal_lines = affected_line.proposal.splitlines(keepends=True)
|
109
|
+
if not proposal_lines:
|
110
|
+
proposal_lines = [""]
|
111
|
+
elif not proposal_lines[-1].endswith(("\n", "\r")):
|
112
|
+
# Ensure the last line has a newline if the original does
|
113
|
+
if (
|
114
|
+
affected_line.end_line < len(lines)
|
115
|
+
and lines[affected_line.end_line - 1].endswith(("\n", "\r"))
|
116
|
+
):
|
117
|
+
proposal_lines[-1] += "\n"
|
118
|
+
|
119
|
+
lines[affected_line.start_line - 1:affected_line.end_line] = proposal_lines
|
120
|
+
|
121
|
+
# Write changes back to the file
|
122
|
+
try:
|
123
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
124
|
+
f.writelines(lines)
|
125
|
+
print(f"{ui.green('Success')}: Changes applied to {file_path}")
|
126
|
+
except Exception as e:
|
127
|
+
logging.error(f"Failed to write changes to {file_path}: {e}")
|
128
|
+
raise typer.Exit(code=1)
|
129
|
+
|
130
|
+
print(f"\n{ui.green('✓')} Issue #{issue_number} fixed successfully")
|
131
|
+
|
132
|
+
changed_files = [file_path.as_posix()]
|
133
|
+
if commit:
|
134
|
+
commit_changes(
|
135
|
+
changed_files,
|
136
|
+
commit_message=f"[AI] Fix issue {issue_number}:{issue.title}",
|
137
|
+
push=push
|
138
|
+
)
|
139
|
+
return changed_files
|
140
|
+
|
141
|
+
|
142
|
+
def commit_changes(
|
143
|
+
files: list[str],
|
144
|
+
repo: git.Repo = None,
|
145
|
+
commit_message: str = "fix by AI",
|
146
|
+
push: bool = True
|
147
|
+
) -> None:
|
148
|
+
if opened_repo := not repo:
|
149
|
+
repo = git.Repo(".")
|
150
|
+
for i in files:
|
151
|
+
repo.index.add(i)
|
152
|
+
repo.index.commit(commit_message)
|
153
|
+
if push:
|
154
|
+
origin = repo.remotes.origin
|
155
|
+
origin.push()
|
156
|
+
logging.info(f"Changes pushed to {origin.name}")
|
157
|
+
else:
|
158
|
+
logging.info("Changes committed but not pushed to remote")
|
159
|
+
if opened_repo:
|
160
|
+
repo.close()
|
@@ -1,111 +1,111 @@
|
|
1
|
-
import logging
|
2
|
-
import os
|
3
|
-
from time import sleep
|
4
|
-
|
5
|
-
import typer
|
6
|
-
from ghapi.core import GhApi
|
7
|
-
|
8
|
-
from ..cli_base import app
|
9
|
-
from ..constants import GITHUB_MD_REPORT_FILE_NAME, HTML_CR_COMMENT_MARKER
|
10
|
-
from ..gh_api import (
|
11
|
-
post_gh_comment,
|
12
|
-
resolve_gh_token,
|
13
|
-
hide_gh_comment,
|
14
|
-
)
|
15
|
-
from ..project_config import ProjectConfig
|
16
|
-
|
17
|
-
|
18
|
-
@app.command(name="github-comment", help="Leave a GitHub PR comment with the review.")
|
19
|
-
def post_github_cr_comment(
|
20
|
-
md_report_file: str = typer.Option(default=None),
|
21
|
-
pr: int = typer.Option(default=None),
|
22
|
-
gh_repo: str = typer.Option(default=None, help="owner/repo"),
|
23
|
-
token: str = typer.Option(
|
24
|
-
"", help="GitHub token (or set GITHUB_TOKEN env var)"
|
25
|
-
),
|
26
|
-
):
|
27
|
-
"""
|
28
|
-
Leaves a comment with the review on the current GitHub pull request.
|
29
|
-
"""
|
30
|
-
file = md_report_file or GITHUB_MD_REPORT_FILE_NAME
|
31
|
-
if not os.path.exists(file):
|
32
|
-
logging.error(f"Review file not found: {file}, comment will not be posted.")
|
33
|
-
raise typer.Exit(4)
|
34
|
-
|
35
|
-
with open(file, "r", encoding="utf-8") as f:
|
36
|
-
body = f.read()
|
37
|
-
|
38
|
-
token = resolve_gh_token(token)
|
39
|
-
if not token:
|
40
|
-
print("GitHub token is required (--token or GITHUB_TOKEN env var).")
|
41
|
-
raise typer.Exit(1)
|
42
|
-
config = ProjectConfig.load()
|
43
|
-
gh_env = config.prompt_vars["github_env"]
|
44
|
-
gh_repo = gh_repo or gh_env.get("github_repo", "")
|
45
|
-
pr_env_val = gh_env.get("github_pr_number", "")
|
46
|
-
logging.info(f"github_pr_number = {pr_env_val}")
|
47
|
-
|
48
|
-
if not pr:
|
49
|
-
# e.g. could be "refs/pull/123/merge" or a direct number
|
50
|
-
if "/" in pr_env_val and "pull" in pr_env_val:
|
51
|
-
# refs/pull/123/merge
|
52
|
-
try:
|
53
|
-
pr_num_candidate = pr_env_val.strip("/").split("/")
|
54
|
-
idx = pr_num_candidate.index("pull")
|
55
|
-
pr = int(pr_num_candidate[idx + 1])
|
56
|
-
except Exception:
|
57
|
-
pass
|
58
|
-
else:
|
59
|
-
try:
|
60
|
-
pr = int(pr_env_val)
|
61
|
-
except ValueError:
|
62
|
-
pass
|
63
|
-
if not pr:
|
64
|
-
if pr_str := os.getenv("PR_NUMBER_FROM_WORKFLOW_DISPATCH"):
|
65
|
-
try:
|
66
|
-
pr = int(pr_str)
|
67
|
-
except ValueError:
|
68
|
-
pass
|
69
|
-
if not pr:
|
70
|
-
logging.error("Could not resolve PR number from environment variables.")
|
71
|
-
raise typer.Exit(3)
|
72
|
-
|
73
|
-
if not post_gh_comment(gh_repo, pr, token, body):
|
74
|
-
raise typer.Exit(5)
|
75
|
-
|
76
|
-
if config.collapse_previous_code_review_comments:
|
77
|
-
sleep(1)
|
78
|
-
collapse_gh_outdated_cr_comments(gh_repo, pr, token)
|
79
|
-
|
80
|
-
|
81
|
-
def collapse_gh_outdated_cr_comments(
|
82
|
-
gh_repository: str,
|
83
|
-
pr_or_issue_number: int,
|
84
|
-
token: str = None
|
85
|
-
):
|
86
|
-
"""
|
87
|
-
Collapse outdated code review comments in a GitHub pull request or issue.
|
88
|
-
"""
|
89
|
-
logging.info(f"Collapsing outdated comments in {gh_repository} #{pr_or_issue_number}...")
|
90
|
-
|
91
|
-
token = resolve_gh_token(token)
|
92
|
-
owner, repo = gh_repository.split('/')
|
93
|
-
api = GhApi(owner, repo, token=token)
|
94
|
-
|
95
|
-
comments = api.issues.list_comments(pr_or_issue_number)
|
96
|
-
review_marker = HTML_CR_COMMENT_MARKER
|
97
|
-
collapsed_title = "🗑️ Outdated Code Review by Gito"
|
98
|
-
collapsed_marker = f"<summary>{collapsed_title}</summary>"
|
99
|
-
outdated_comments = [
|
100
|
-
c for c in comments
|
101
|
-
if c.body and review_marker in c.body and collapsed_marker not in c.body
|
102
|
-
][:-1]
|
103
|
-
if not outdated_comments:
|
104
|
-
logging.info("No outdated comments found")
|
105
|
-
return
|
106
|
-
for comment in outdated_comments:
|
107
|
-
logging.info(f"Collapsing comment {comment.id}...")
|
108
|
-
new_body = f"<details>\n<summary>{collapsed_title}</summary>\n\n{comment.body}\n</details>"
|
109
|
-
api.issues.update_comment(comment.id, new_body)
|
110
|
-
hide_gh_comment(comment.node_id, token)
|
111
|
-
logging.info("All outdated comments collapsed successfully.")
|
1
|
+
import logging
|
2
|
+
import os
|
3
|
+
from time import sleep
|
4
|
+
|
5
|
+
import typer
|
6
|
+
from ghapi.core import GhApi
|
7
|
+
|
8
|
+
from ..cli_base import app
|
9
|
+
from ..constants import GITHUB_MD_REPORT_FILE_NAME, HTML_CR_COMMENT_MARKER
|
10
|
+
from ..gh_api import (
|
11
|
+
post_gh_comment,
|
12
|
+
resolve_gh_token,
|
13
|
+
hide_gh_comment,
|
14
|
+
)
|
15
|
+
from ..project_config import ProjectConfig
|
16
|
+
|
17
|
+
|
18
|
+
@app.command(name="github-comment", help="Leave a GitHub PR comment with the review.")
|
19
|
+
def post_github_cr_comment(
|
20
|
+
md_report_file: str = typer.Option(default=None),
|
21
|
+
pr: int = typer.Option(default=None),
|
22
|
+
gh_repo: str = typer.Option(default=None, help="owner/repo"),
|
23
|
+
token: str = typer.Option(
|
24
|
+
"", help="GitHub token (or set GITHUB_TOKEN env var)"
|
25
|
+
),
|
26
|
+
):
|
27
|
+
"""
|
28
|
+
Leaves a comment with the review on the current GitHub pull request.
|
29
|
+
"""
|
30
|
+
file = md_report_file or GITHUB_MD_REPORT_FILE_NAME
|
31
|
+
if not os.path.exists(file):
|
32
|
+
logging.error(f"Review file not found: {file}, comment will not be posted.")
|
33
|
+
raise typer.Exit(4)
|
34
|
+
|
35
|
+
with open(file, "r", encoding="utf-8") as f:
|
36
|
+
body = f.read()
|
37
|
+
|
38
|
+
token = resolve_gh_token(token)
|
39
|
+
if not token:
|
40
|
+
print("GitHub token is required (--token or GITHUB_TOKEN env var).")
|
41
|
+
raise typer.Exit(1)
|
42
|
+
config = ProjectConfig.load()
|
43
|
+
gh_env = config.prompt_vars["github_env"]
|
44
|
+
gh_repo = gh_repo or gh_env.get("github_repo", "")
|
45
|
+
pr_env_val = gh_env.get("github_pr_number", "")
|
46
|
+
logging.info(f"github_pr_number = {pr_env_val}")
|
47
|
+
|
48
|
+
if not pr:
|
49
|
+
# e.g. could be "refs/pull/123/merge" or a direct number
|
50
|
+
if "/" in pr_env_val and "pull" in pr_env_val:
|
51
|
+
# refs/pull/123/merge
|
52
|
+
try:
|
53
|
+
pr_num_candidate = pr_env_val.strip("/").split("/")
|
54
|
+
idx = pr_num_candidate.index("pull")
|
55
|
+
pr = int(pr_num_candidate[idx + 1])
|
56
|
+
except Exception:
|
57
|
+
pass
|
58
|
+
else:
|
59
|
+
try:
|
60
|
+
pr = int(pr_env_val)
|
61
|
+
except ValueError:
|
62
|
+
pass
|
63
|
+
if not pr:
|
64
|
+
if pr_str := os.getenv("PR_NUMBER_FROM_WORKFLOW_DISPATCH"):
|
65
|
+
try:
|
66
|
+
pr = int(pr_str)
|
67
|
+
except ValueError:
|
68
|
+
pass
|
69
|
+
if not pr:
|
70
|
+
logging.error("Could not resolve PR number from environment variables.")
|
71
|
+
raise typer.Exit(3)
|
72
|
+
|
73
|
+
if not post_gh_comment(gh_repo, pr, token, body):
|
74
|
+
raise typer.Exit(5)
|
75
|
+
|
76
|
+
if config.collapse_previous_code_review_comments:
|
77
|
+
sleep(1)
|
78
|
+
collapse_gh_outdated_cr_comments(gh_repo, pr, token)
|
79
|
+
|
80
|
+
|
81
|
+
def collapse_gh_outdated_cr_comments(
|
82
|
+
gh_repository: str,
|
83
|
+
pr_or_issue_number: int,
|
84
|
+
token: str = None
|
85
|
+
):
|
86
|
+
"""
|
87
|
+
Collapse outdated code review comments in a GitHub pull request or issue.
|
88
|
+
"""
|
89
|
+
logging.info(f"Collapsing outdated comments in {gh_repository} #{pr_or_issue_number}...")
|
90
|
+
|
91
|
+
token = resolve_gh_token(token)
|
92
|
+
owner, repo = gh_repository.split('/')
|
93
|
+
api = GhApi(owner, repo, token=token)
|
94
|
+
|
95
|
+
comments = api.issues.list_comments(pr_or_issue_number)
|
96
|
+
review_marker = HTML_CR_COMMENT_MARKER
|
97
|
+
collapsed_title = "🗑️ Outdated Code Review by Gito"
|
98
|
+
collapsed_marker = f"<summary>{collapsed_title}</summary>"
|
99
|
+
outdated_comments = [
|
100
|
+
c for c in comments
|
101
|
+
if c.body and review_marker in c.body and collapsed_marker not in c.body
|
102
|
+
][:-1]
|
103
|
+
if not outdated_comments:
|
104
|
+
logging.info("No outdated comments found")
|
105
|
+
return
|
106
|
+
for comment in outdated_comments:
|
107
|
+
logging.info(f"Collapsing comment {comment.id}...")
|
108
|
+
new_body = f"<details>\n<summary>{collapsed_title}</summary>\n\n{comment.body}\n</details>"
|
109
|
+
api.issues.update_comment(comment.id, new_body)
|
110
|
+
hide_gh_comment(comment.node_id, token)
|
111
|
+
logging.info("All outdated comments collapsed successfully.")
|