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/pipeline_steps/linear.py
CHANGED
@@ -1,85 +1,85 @@
|
|
1
|
-
import logging
|
2
|
-
import os
|
3
|
-
import requests
|
4
|
-
|
5
|
-
import git
|
6
|
-
|
7
|
-
from gito.issue_trackers import IssueTrackerIssue, resolve_issue_key
|
8
|
-
|
9
|
-
|
10
|
-
def fetch_issue(issue_key: str, api_key: str = None) -> IssueTrackerIssue | None:
|
11
|
-
"""
|
12
|
-
Fetch a Linear issue using GraphQL API.
|
13
|
-
"""
|
14
|
-
api_key = api_key or os.getenv("LINEAR_API_KEY")
|
15
|
-
try:
|
16
|
-
url = "https://api.linear.app/graphql"
|
17
|
-
headers = {
|
18
|
-
"Authorization": f"{api_key}",
|
19
|
-
"Content-Type": "application/json"
|
20
|
-
}
|
21
|
-
|
22
|
-
query = """
|
23
|
-
query Issues($teamKey: String!, $issueNumber: Float) {
|
24
|
-
issues(filter: {team: {key: {eq: $teamKey}}, number: {eq: $issueNumber}}) {
|
25
|
-
nodes {
|
26
|
-
id
|
27
|
-
identifier
|
28
|
-
title
|
29
|
-
description
|
30
|
-
url
|
31
|
-
}
|
32
|
-
}
|
33
|
-
}
|
34
|
-
"""
|
35
|
-
team_key, issue_number = issue_key.split("-")
|
36
|
-
response = requests.post(
|
37
|
-
url,
|
38
|
-
json={
|
39
|
-
"query": query,
|
40
|
-
"variables": {'teamKey': team_key, 'issueNumber': int(issue_number)}
|
41
|
-
},
|
42
|
-
headers=headers
|
43
|
-
)
|
44
|
-
response.raise_for_status()
|
45
|
-
data = response.json()
|
46
|
-
|
47
|
-
if "errors" in data:
|
48
|
-
logging.error(f"Linear API error: {data['errors']}")
|
49
|
-
return None
|
50
|
-
|
51
|
-
nodes = data.get("data", {}).get("issues", {}).get("nodes", [])
|
52
|
-
if not nodes:
|
53
|
-
logging.error(f"Linear issue {issue_key} not found")
|
54
|
-
return None
|
55
|
-
|
56
|
-
issue = nodes[0]
|
57
|
-
return IssueTrackerIssue(
|
58
|
-
title=issue["title"],
|
59
|
-
description=issue.get("description") or "",
|
60
|
-
url=issue["url"]
|
61
|
-
)
|
62
|
-
|
63
|
-
except requests.HTTPError as e:
|
64
|
-
logging.error(f"Failed to fetch Linear issue {issue_key}: {e}")
|
65
|
-
logging.error(f"Response body: {response.text}")
|
66
|
-
return None
|
67
|
-
|
68
|
-
|
69
|
-
def fetch_associated_issue(
|
70
|
-
repo: git.Repo,
|
71
|
-
api_key=None,
|
72
|
-
**kwargs
|
73
|
-
):
|
74
|
-
"""
|
75
|
-
Pipeline step to fetch a Linear issue based on the current branch name.
|
76
|
-
"""
|
77
|
-
api_key = api_key or os.getenv("LINEAR_API_KEY")
|
78
|
-
if not api_key:
|
79
|
-
logging.error("LINEAR_API_KEY environment variable is not set")
|
80
|
-
return
|
81
|
-
|
82
|
-
issue_key = resolve_issue_key(repo)
|
83
|
-
return dict(
|
84
|
-
associated_issue=fetch_issue(issue_key, api_key)
|
85
|
-
) if issue_key else None
|
1
|
+
import logging
|
2
|
+
import os
|
3
|
+
import requests
|
4
|
+
|
5
|
+
import git
|
6
|
+
|
7
|
+
from gito.issue_trackers import IssueTrackerIssue, resolve_issue_key
|
8
|
+
|
9
|
+
|
10
|
+
def fetch_issue(issue_key: str, api_key: str = None) -> IssueTrackerIssue | None:
|
11
|
+
"""
|
12
|
+
Fetch a Linear issue using GraphQL API.
|
13
|
+
"""
|
14
|
+
api_key = api_key or os.getenv("LINEAR_API_KEY")
|
15
|
+
try:
|
16
|
+
url = "https://api.linear.app/graphql"
|
17
|
+
headers = {
|
18
|
+
"Authorization": f"{api_key}",
|
19
|
+
"Content-Type": "application/json"
|
20
|
+
}
|
21
|
+
|
22
|
+
query = """
|
23
|
+
query Issues($teamKey: String!, $issueNumber: Float) {
|
24
|
+
issues(filter: {team: {key: {eq: $teamKey}}, number: {eq: $issueNumber}}) {
|
25
|
+
nodes {
|
26
|
+
id
|
27
|
+
identifier
|
28
|
+
title
|
29
|
+
description
|
30
|
+
url
|
31
|
+
}
|
32
|
+
}
|
33
|
+
}
|
34
|
+
"""
|
35
|
+
team_key, issue_number = issue_key.split("-")
|
36
|
+
response = requests.post(
|
37
|
+
url,
|
38
|
+
json={
|
39
|
+
"query": query,
|
40
|
+
"variables": {'teamKey': team_key, 'issueNumber': int(issue_number)}
|
41
|
+
},
|
42
|
+
headers=headers
|
43
|
+
)
|
44
|
+
response.raise_for_status()
|
45
|
+
data = response.json()
|
46
|
+
|
47
|
+
if "errors" in data:
|
48
|
+
logging.error(f"Linear API error: {data['errors']}")
|
49
|
+
return None
|
50
|
+
|
51
|
+
nodes = data.get("data", {}).get("issues", {}).get("nodes", [])
|
52
|
+
if not nodes:
|
53
|
+
logging.error(f"Linear issue {issue_key} not found")
|
54
|
+
return None
|
55
|
+
|
56
|
+
issue = nodes[0]
|
57
|
+
return IssueTrackerIssue(
|
58
|
+
title=issue["title"],
|
59
|
+
description=issue.get("description") or "",
|
60
|
+
url=issue["url"]
|
61
|
+
)
|
62
|
+
|
63
|
+
except requests.HTTPError as e:
|
64
|
+
logging.error(f"Failed to fetch Linear issue {issue_key}: {e}")
|
65
|
+
logging.error(f"Response body: {response.text}")
|
66
|
+
return None
|
67
|
+
|
68
|
+
|
69
|
+
def fetch_associated_issue(
|
70
|
+
repo: git.Repo,
|
71
|
+
api_key=None,
|
72
|
+
**kwargs
|
73
|
+
):
|
74
|
+
"""
|
75
|
+
Pipeline step to fetch a Linear issue based on the current branch name.
|
76
|
+
"""
|
77
|
+
api_key = api_key or os.getenv("LINEAR_API_KEY")
|
78
|
+
if not api_key:
|
79
|
+
logging.error("LINEAR_API_KEY environment variable is not set")
|
80
|
+
return
|
81
|
+
|
82
|
+
issue_key = resolve_issue_key(repo)
|
83
|
+
return dict(
|
84
|
+
associated_issue=fetch_issue(issue_key, api_key)
|
85
|
+
) if issue_key else None
|
gito/project_config.py
CHANGED
@@ -1,85 +1,85 @@
|
|
1
|
-
import logging
|
2
|
-
import tomllib
|
3
|
-
from dataclasses import dataclass, field
|
4
|
-
from pathlib import Path
|
5
|
-
|
6
|
-
import microcore as mc
|
7
|
-
from gito.utils import detect_github_env
|
8
|
-
from microcore import ui
|
9
|
-
from git import Repo
|
10
|
-
|
11
|
-
from .constants import PROJECT_CONFIG_BUNDLED_DEFAULTS_FILE, PROJECT_CONFIG_FILE_PATH
|
12
|
-
from .pipeline import PipelineStep
|
13
|
-
|
14
|
-
|
15
|
-
@dataclass
|
16
|
-
class ProjectConfig:
|
17
|
-
prompt: str = ""
|
18
|
-
summary_prompt: str = ""
|
19
|
-
answer_prompt: str = ""
|
20
|
-
report_template_md: str = ""
|
21
|
-
"""Markdown report template"""
|
22
|
-
report_template_cli: str = ""
|
23
|
-
"""Report template for CLI output"""
|
24
|
-
post_process: str = ""
|
25
|
-
retries: int = 3
|
26
|
-
"""LLM retries for one request"""
|
27
|
-
max_code_tokens: int = 32000
|
28
|
-
prompt_vars: dict = field(default_factory=dict)
|
29
|
-
mention_triggers: list[str] = field(default_factory=list)
|
30
|
-
answer_github_comments: bool = field(default=True)
|
31
|
-
"""
|
32
|
-
Defines the keyword or mention tag that triggers bot actions
|
33
|
-
when referenced in code review comments.
|
34
|
-
"""
|
35
|
-
aux_files: list[str] = field(default_factory=list)
|
36
|
-
pipeline_steps: dict[str, dict | PipelineStep] = field(default_factory=dict)
|
37
|
-
collapse_previous_code_review_comments: bool = field(default=True)
|
38
|
-
"""
|
39
|
-
If True, previously added code review comments in the pull request
|
40
|
-
will be collapsed automatically when a new comment is added.
|
41
|
-
"""
|
42
|
-
|
43
|
-
def __post_init__(self):
|
44
|
-
self.pipeline_steps = {
|
45
|
-
k: PipelineStep(**v) if isinstance(v, dict) else v
|
46
|
-
for k, v in self.pipeline_steps.items()
|
47
|
-
}
|
48
|
-
|
49
|
-
@staticmethod
|
50
|
-
def _read_bundled_defaults() -> dict:
|
51
|
-
with open(PROJECT_CONFIG_BUNDLED_DEFAULTS_FILE, "rb") as f:
|
52
|
-
config = tomllib.load(f)
|
53
|
-
return config
|
54
|
-
|
55
|
-
@staticmethod
|
56
|
-
def load_for_repo(repo: Repo):
|
57
|
-
return ProjectConfig.load(Path(repo.working_tree_dir) / PROJECT_CONFIG_FILE_PATH)
|
58
|
-
|
59
|
-
@staticmethod
|
60
|
-
def load(config_path: str | Path | None = None) -> "ProjectConfig":
|
61
|
-
config = ProjectConfig._read_bundled_defaults()
|
62
|
-
github_env = detect_github_env()
|
63
|
-
config["prompt_vars"] |= github_env | dict(github_env=github_env)
|
64
|
-
|
65
|
-
config_path = Path(config_path or PROJECT_CONFIG_FILE_PATH)
|
66
|
-
if config_path.exists():
|
67
|
-
logging.info(
|
68
|
-
f"Loading project-specific configuration from {mc.utils.file_link(config_path)}...")
|
69
|
-
default_prompt_vars = config["prompt_vars"]
|
70
|
-
default_pipeline_steps = config["pipeline_steps"]
|
71
|
-
with open(config_path, "rb") as f:
|
72
|
-
config.update(tomllib.load(f))
|
73
|
-
# overriding prompt_vars config section will not empty default values
|
74
|
-
config["prompt_vars"] = default_prompt_vars | config["prompt_vars"]
|
75
|
-
# merge individual pipeline steps
|
76
|
-
for k, v in config["pipeline_steps"].items():
|
77
|
-
config["pipeline_steps"][k] = default_pipeline_steps.get(k, {}) | v
|
78
|
-
# merge pipeline steps dict
|
79
|
-
config["pipeline_steps"] = default_pipeline_steps | config["pipeline_steps"]
|
80
|
-
else:
|
81
|
-
logging.info(
|
82
|
-
f"No project config found at {ui.blue(config_path)}, using defaults"
|
83
|
-
)
|
84
|
-
|
85
|
-
return ProjectConfig(**config)
|
1
|
+
import logging
|
2
|
+
import tomllib
|
3
|
+
from dataclasses import dataclass, field
|
4
|
+
from pathlib import Path
|
5
|
+
|
6
|
+
import microcore as mc
|
7
|
+
from gito.utils import detect_github_env
|
8
|
+
from microcore import ui
|
9
|
+
from git import Repo
|
10
|
+
|
11
|
+
from .constants import PROJECT_CONFIG_BUNDLED_DEFAULTS_FILE, PROJECT_CONFIG_FILE_PATH
|
12
|
+
from .pipeline import PipelineStep
|
13
|
+
|
14
|
+
|
15
|
+
@dataclass
|
16
|
+
class ProjectConfig:
|
17
|
+
prompt: str = ""
|
18
|
+
summary_prompt: str = ""
|
19
|
+
answer_prompt: str = ""
|
20
|
+
report_template_md: str = ""
|
21
|
+
"""Markdown report template"""
|
22
|
+
report_template_cli: str = ""
|
23
|
+
"""Report template for CLI output"""
|
24
|
+
post_process: str = ""
|
25
|
+
retries: int = 3
|
26
|
+
"""LLM retries for one request"""
|
27
|
+
max_code_tokens: int = 32000
|
28
|
+
prompt_vars: dict = field(default_factory=dict)
|
29
|
+
mention_triggers: list[str] = field(default_factory=list)
|
30
|
+
answer_github_comments: bool = field(default=True)
|
31
|
+
"""
|
32
|
+
Defines the keyword or mention tag that triggers bot actions
|
33
|
+
when referenced in code review comments.
|
34
|
+
"""
|
35
|
+
aux_files: list[str] = field(default_factory=list)
|
36
|
+
pipeline_steps: dict[str, dict | PipelineStep] = field(default_factory=dict)
|
37
|
+
collapse_previous_code_review_comments: bool = field(default=True)
|
38
|
+
"""
|
39
|
+
If True, previously added code review comments in the pull request
|
40
|
+
will be collapsed automatically when a new comment is added.
|
41
|
+
"""
|
42
|
+
|
43
|
+
def __post_init__(self):
|
44
|
+
self.pipeline_steps = {
|
45
|
+
k: PipelineStep(**v) if isinstance(v, dict) else v
|
46
|
+
for k, v in self.pipeline_steps.items()
|
47
|
+
}
|
48
|
+
|
49
|
+
@staticmethod
|
50
|
+
def _read_bundled_defaults() -> dict:
|
51
|
+
with open(PROJECT_CONFIG_BUNDLED_DEFAULTS_FILE, "rb") as f:
|
52
|
+
config = tomllib.load(f)
|
53
|
+
return config
|
54
|
+
|
55
|
+
@staticmethod
|
56
|
+
def load_for_repo(repo: Repo):
|
57
|
+
return ProjectConfig.load(Path(repo.working_tree_dir) / PROJECT_CONFIG_FILE_PATH)
|
58
|
+
|
59
|
+
@staticmethod
|
60
|
+
def load(config_path: str | Path | None = None) -> "ProjectConfig":
|
61
|
+
config = ProjectConfig._read_bundled_defaults()
|
62
|
+
github_env = detect_github_env()
|
63
|
+
config["prompt_vars"] |= github_env | dict(github_env=github_env)
|
64
|
+
|
65
|
+
config_path = Path(config_path or PROJECT_CONFIG_FILE_PATH)
|
66
|
+
if config_path.exists():
|
67
|
+
logging.info(
|
68
|
+
f"Loading project-specific configuration from {mc.utils.file_link(config_path)}...")
|
69
|
+
default_prompt_vars = config["prompt_vars"]
|
70
|
+
default_pipeline_steps = config["pipeline_steps"]
|
71
|
+
with open(config_path, "rb") as f:
|
72
|
+
config.update(tomllib.load(f))
|
73
|
+
# overriding prompt_vars config section will not empty default values
|
74
|
+
config["prompt_vars"] = default_prompt_vars | config["prompt_vars"]
|
75
|
+
# merge individual pipeline steps
|
76
|
+
for k, v in config["pipeline_steps"].items():
|
77
|
+
config["pipeline_steps"][k] = default_pipeline_steps.get(k, {}) | v
|
78
|
+
# merge pipeline steps dict
|
79
|
+
config["pipeline_steps"] = default_pipeline_steps | config["pipeline_steps"]
|
80
|
+
else:
|
81
|
+
logging.info(
|
82
|
+
f"No project config found at {ui.blue(config_path)}, using defaults"
|
83
|
+
)
|
84
|
+
|
85
|
+
return ProjectConfig(**config)
|
gito/report_struct.py
CHANGED
@@ -1,136 +1,136 @@
|
|
1
|
-
import json
|
2
|
-
import logging
|
3
|
-
from dataclasses import dataclass, field, asdict
|
4
|
-
from datetime import datetime
|
5
|
-
from enum import StrEnum
|
6
|
-
from pathlib import Path
|
7
|
-
|
8
|
-
import microcore as mc
|
9
|
-
from colorama import Fore, Style, Back
|
10
|
-
from microcore.utils import file_link
|
11
|
-
import textwrap
|
12
|
-
|
13
|
-
from .constants import JSON_REPORT_FILE_NAME, HTML_TEXT_ICON, HTML_CR_COMMENT_MARKER
|
14
|
-
from .project_config import ProjectConfig
|
15
|
-
from .utils import syntax_hint, block_wrap_lr, max_line_len, remove_html_comments
|
16
|
-
|
17
|
-
|
18
|
-
@dataclass
|
19
|
-
class Issue:
|
20
|
-
@dataclass
|
21
|
-
class AffectedCode:
|
22
|
-
start_line: int = field()
|
23
|
-
end_line: int | None = field(default=None)
|
24
|
-
file: str = field(default="")
|
25
|
-
proposal: str = field(default="")
|
26
|
-
affected_code: str = field(default="")
|
27
|
-
|
28
|
-
@property
|
29
|
-
def syntax_hint(self) -> str:
|
30
|
-
return syntax_hint(self.file)
|
31
|
-
|
32
|
-
id: str = field()
|
33
|
-
title: str = field()
|
34
|
-
details: str = field(default="")
|
35
|
-
severity: int | None = field(default=None)
|
36
|
-
confidence: int | None = field(default=None)
|
37
|
-
tags: list[str] = field(default_factory=list)
|
38
|
-
file: str = field(default="")
|
39
|
-
affected_lines: list[AffectedCode] = field(default_factory=list)
|
40
|
-
|
41
|
-
def __post_init__(self):
|
42
|
-
self.affected_lines = [
|
43
|
-
Issue.AffectedCode(**dict(file=self.file) | i)
|
44
|
-
for i in self.affected_lines
|
45
|
-
]
|
46
|
-
|
47
|
-
def github_code_link(self, github_env: dict) -> str:
|
48
|
-
url = (
|
49
|
-
f"https://github.com/{github_env['github_repo']}"
|
50
|
-
f"/blob/{github_env['github_pr_sha_or_branch']}"
|
51
|
-
f"/{self.file}"
|
52
|
-
)
|
53
|
-
if self.affected_lines:
|
54
|
-
url += f"#L{self.affected_lines[0].start_line}"
|
55
|
-
if self.affected_lines[0].end_line:
|
56
|
-
url += f"-L{self.affected_lines[0].end_line}"
|
57
|
-
return url
|
58
|
-
|
59
|
-
|
60
|
-
@dataclass
|
61
|
-
class Report:
|
62
|
-
class Format(StrEnum):
|
63
|
-
MARKDOWN = "md"
|
64
|
-
CLI = "cli"
|
65
|
-
|
66
|
-
issues: dict[str, list[Issue]] = field(default_factory=dict)
|
67
|
-
summary: str = field(default="")
|
68
|
-
number_of_processed_files: int = field(default=0)
|
69
|
-
total_issues: int = field(init=False)
|
70
|
-
created_at: str = field(default_factory=lambda: datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
71
|
-
model: str = field(default_factory=lambda: mc.config().MODEL)
|
72
|
-
pipeline_out: dict = field(default_factory=dict)
|
73
|
-
|
74
|
-
@property
|
75
|
-
def plain_issues(self):
|
76
|
-
return [
|
77
|
-
issue
|
78
|
-
for file, issues in self.issues.items()
|
79
|
-
for issue in issues
|
80
|
-
]
|
81
|
-
|
82
|
-
def __post_init__(self):
|
83
|
-
issue_id: int = 0
|
84
|
-
for file in self.issues.keys():
|
85
|
-
self.issues[file] = [
|
86
|
-
Issue(
|
87
|
-
**{
|
88
|
-
"id": (issue_id := issue_id + 1),
|
89
|
-
"file": file,
|
90
|
-
} | issue
|
91
|
-
)
|
92
|
-
for issue in self.issues[file]
|
93
|
-
]
|
94
|
-
self.total_issues = issue_id
|
95
|
-
|
96
|
-
def save(self, file_name: str = ""):
|
97
|
-
file_name = file_name or JSON_REPORT_FILE_NAME
|
98
|
-
with open(file_name, "w") as f:
|
99
|
-
json.dump(asdict(self), f, indent=4)
|
100
|
-
logging.info(f"Report saved to {mc.utils.file_link(file_name)}")
|
101
|
-
|
102
|
-
@staticmethod
|
103
|
-
def load(file_name: str | Path = ""):
|
104
|
-
with open(file_name or JSON_REPORT_FILE_NAME, "r") as f:
|
105
|
-
data = json.load(f)
|
106
|
-
data.pop("total_issues", None)
|
107
|
-
return Report(**data)
|
108
|
-
|
109
|
-
def render(
|
110
|
-
self,
|
111
|
-
config: ProjectConfig = None,
|
112
|
-
report_format: Format = Format.MARKDOWN,
|
113
|
-
) -> str:
|
114
|
-
config = config or ProjectConfig.load()
|
115
|
-
template = getattr(config, f"report_template_{report_format}")
|
116
|
-
return mc.prompt(
|
117
|
-
template,
|
118
|
-
report=self,
|
119
|
-
ui=mc.ui,
|
120
|
-
Fore=Fore,
|
121
|
-
Style=Style,
|
122
|
-
Back=Back,
|
123
|
-
file_link=file_link,
|
124
|
-
textwrap=textwrap,
|
125
|
-
block_wrap_lr=block_wrap_lr,
|
126
|
-
max_line_len=max_line_len,
|
127
|
-
HTML_TEXT_ICON=HTML_TEXT_ICON,
|
128
|
-
HTML_CR_COMMENT_MARKER=HTML_CR_COMMENT_MARKER,
|
129
|
-
remove_html_comments=remove_html_comments,
|
130
|
-
**config.prompt_vars
|
131
|
-
)
|
132
|
-
|
133
|
-
def to_cli(self, report_format=Format.CLI):
|
134
|
-
output = self.render(report_format=report_format)
|
135
|
-
print("")
|
136
|
-
print(output)
|
1
|
+
import json
|
2
|
+
import logging
|
3
|
+
from dataclasses import dataclass, field, asdict
|
4
|
+
from datetime import datetime
|
5
|
+
from enum import StrEnum
|
6
|
+
from pathlib import Path
|
7
|
+
|
8
|
+
import microcore as mc
|
9
|
+
from colorama import Fore, Style, Back
|
10
|
+
from microcore.utils import file_link
|
11
|
+
import textwrap
|
12
|
+
|
13
|
+
from .constants import JSON_REPORT_FILE_NAME, HTML_TEXT_ICON, HTML_CR_COMMENT_MARKER
|
14
|
+
from .project_config import ProjectConfig
|
15
|
+
from .utils import syntax_hint, block_wrap_lr, max_line_len, remove_html_comments, filter_kwargs
|
16
|
+
|
17
|
+
|
18
|
+
@dataclass
|
19
|
+
class Issue:
|
20
|
+
@dataclass
|
21
|
+
class AffectedCode:
|
22
|
+
start_line: int = field()
|
23
|
+
end_line: int | None = field(default=None)
|
24
|
+
file: str = field(default="")
|
25
|
+
proposal: str = field(default="")
|
26
|
+
affected_code: str = field(default="")
|
27
|
+
|
28
|
+
@property
|
29
|
+
def syntax_hint(self) -> str:
|
30
|
+
return syntax_hint(self.file)
|
31
|
+
|
32
|
+
id: str = field()
|
33
|
+
title: str = field()
|
34
|
+
details: str = field(default="")
|
35
|
+
severity: int | None = field(default=None)
|
36
|
+
confidence: int | None = field(default=None)
|
37
|
+
tags: list[str] = field(default_factory=list)
|
38
|
+
file: str = field(default="")
|
39
|
+
affected_lines: list[AffectedCode] = field(default_factory=list)
|
40
|
+
|
41
|
+
def __post_init__(self):
|
42
|
+
self.affected_lines = [
|
43
|
+
Issue.AffectedCode(**filter_kwargs(Issue.AffectedCode, dict(file=self.file) | i))
|
44
|
+
for i in self.affected_lines
|
45
|
+
]
|
46
|
+
|
47
|
+
def github_code_link(self, github_env: dict) -> str:
|
48
|
+
url = (
|
49
|
+
f"https://github.com/{github_env['github_repo']}"
|
50
|
+
f"/blob/{github_env['github_pr_sha_or_branch']}"
|
51
|
+
f"/{self.file}"
|
52
|
+
)
|
53
|
+
if self.affected_lines:
|
54
|
+
url += f"#L{self.affected_lines[0].start_line}"
|
55
|
+
if self.affected_lines[0].end_line:
|
56
|
+
url += f"-L{self.affected_lines[0].end_line}"
|
57
|
+
return url
|
58
|
+
|
59
|
+
|
60
|
+
@dataclass
|
61
|
+
class Report:
|
62
|
+
class Format(StrEnum):
|
63
|
+
MARKDOWN = "md"
|
64
|
+
CLI = "cli"
|
65
|
+
|
66
|
+
issues: dict[str, list[Issue | dict]] = field(default_factory=dict)
|
67
|
+
summary: str = field(default="")
|
68
|
+
number_of_processed_files: int = field(default=0)
|
69
|
+
total_issues: int = field(init=False)
|
70
|
+
created_at: str = field(default_factory=lambda: datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
71
|
+
model: str = field(default_factory=lambda: mc.config().MODEL)
|
72
|
+
pipeline_out: dict = field(default_factory=dict)
|
73
|
+
|
74
|
+
@property
|
75
|
+
def plain_issues(self):
|
76
|
+
return [
|
77
|
+
issue
|
78
|
+
for file, issues in self.issues.items()
|
79
|
+
for issue in issues
|
80
|
+
]
|
81
|
+
|
82
|
+
def __post_init__(self):
|
83
|
+
issue_id: int = 0
|
84
|
+
for file in self.issues.keys():
|
85
|
+
self.issues[file] = [
|
86
|
+
Issue(
|
87
|
+
**filter_kwargs(Issue, {
|
88
|
+
"id": (issue_id := issue_id + 1),
|
89
|
+
"file": file,
|
90
|
+
} | issue)
|
91
|
+
)
|
92
|
+
for issue in self.issues[file]
|
93
|
+
]
|
94
|
+
self.total_issues = issue_id
|
95
|
+
|
96
|
+
def save(self, file_name: str = ""):
|
97
|
+
file_name = file_name or JSON_REPORT_FILE_NAME
|
98
|
+
with open(file_name, "w") as f:
|
99
|
+
json.dump(asdict(self), f, indent=4)
|
100
|
+
logging.info(f"Report saved to {mc.utils.file_link(file_name)}")
|
101
|
+
|
102
|
+
@staticmethod
|
103
|
+
def load(file_name: str | Path = ""):
|
104
|
+
with open(file_name or JSON_REPORT_FILE_NAME, "r") as f:
|
105
|
+
data = json.load(f)
|
106
|
+
data.pop("total_issues", None)
|
107
|
+
return Report(**data)
|
108
|
+
|
109
|
+
def render(
|
110
|
+
self,
|
111
|
+
config: ProjectConfig = None,
|
112
|
+
report_format: Format = Format.MARKDOWN,
|
113
|
+
) -> str:
|
114
|
+
config = config or ProjectConfig.load()
|
115
|
+
template = getattr(config, f"report_template_{report_format}")
|
116
|
+
return mc.prompt(
|
117
|
+
template,
|
118
|
+
report=self,
|
119
|
+
ui=mc.ui,
|
120
|
+
Fore=Fore,
|
121
|
+
Style=Style,
|
122
|
+
Back=Back,
|
123
|
+
file_link=file_link,
|
124
|
+
textwrap=textwrap,
|
125
|
+
block_wrap_lr=block_wrap_lr,
|
126
|
+
max_line_len=max_line_len,
|
127
|
+
HTML_TEXT_ICON=HTML_TEXT_ICON,
|
128
|
+
HTML_CR_COMMENT_MARKER=HTML_CR_COMMENT_MARKER,
|
129
|
+
remove_html_comments=remove_html_comments,
|
130
|
+
**config.prompt_vars
|
131
|
+
)
|
132
|
+
|
133
|
+
def to_cli(self, report_format=Format.CLI):
|
134
|
+
output = self.render(report_format=report_format)
|
135
|
+
print("")
|
136
|
+
print(output)
|
gito/tpl/answer.j2
CHANGED
@@ -1,26 +1,26 @@
|
|
1
|
-
{{ self_id }}
|
2
|
-
----TASK----
|
3
|
-
Answer the following user question:
|
4
|
-
--USER QUESTION--
|
5
|
-
{{ question }}
|
6
|
-
----
|
7
|
-
----RELATED CODEBASE CHANGES----
|
8
|
-
{% for part in diff %}{{ part }}\n{% endfor %}
|
9
|
-
|
10
|
-
----FULL FILE CONTENT AFTER APPLYING CHANGES----
|
11
|
-
{% for file, file_lines in all_file_lines.items() %}
|
12
|
-
--FILE: {{ file }}--
|
13
|
-
{{ file_lines }}
|
14
|
-
{% endfor %}
|
15
|
-
|
16
|
-
{% include "partial/aux_files.j2" %}
|
17
|
-
|
18
|
-
{%- if pipeline_out.associated_issue and pipeline_out.associated_issue.title %}
|
19
|
-
----ASSOCIATED ISSUE----
|
20
|
-
# {{ pipeline_out.associated_issue.title }}
|
21
|
-
{{ pipeline_out.associated_issue.description }}
|
22
|
-
URL: {{ pipeline_out.associated_issue.url }}
|
23
|
-
{%- endif -%}{{ '\n' }}
|
24
|
-
|
25
|
-
----ANSWERING INSTRUCTIONS----
|
1
|
+
{{ self_id }}
|
2
|
+
----TASK----
|
3
|
+
Answer the following user question:
|
4
|
+
--USER QUESTION--
|
5
|
+
{{ question }}
|
6
|
+
----
|
7
|
+
----RELATED CODEBASE CHANGES----
|
8
|
+
{% for part in diff %}{{ part }}\n{% endfor %}
|
9
|
+
|
10
|
+
----FULL FILE CONTENT AFTER APPLYING CHANGES----
|
11
|
+
{% for file, file_lines in all_file_lines.items() %}
|
12
|
+
--FILE: {{ file }}--
|
13
|
+
{{ file_lines }}
|
14
|
+
{% endfor %}
|
15
|
+
|
16
|
+
{% include "partial/aux_files.j2" %}
|
17
|
+
|
18
|
+
{%- if pipeline_out.associated_issue and pipeline_out.associated_issue.title %}
|
19
|
+
----ASSOCIATED ISSUE----
|
20
|
+
# {{ pipeline_out.associated_issue.title }}
|
21
|
+
{{ pipeline_out.associated_issue.description }}
|
22
|
+
URL: {{ pipeline_out.associated_issue.url }}
|
23
|
+
{%- endif -%}{{ '\n' }}
|
24
|
+
|
25
|
+
----ANSWERING INSTRUCTIONS----
|
26
26
|
{{ answering_instructions }}
|