ai-cr 2.0.0.dev2__py3-none-any.whl → 2.0.1__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-2.0.0.dev2.dist-info → ai_cr-2.0.1.dist-info}/LICENSE +21 -21
- {ai_cr-2.0.0.dev2.dist-info → ai_cr-2.0.1.dist-info}/METADATA +11 -3
- ai_cr-2.0.1.dist-info/RECORD +26 -0
- {ai_cr-2.0.0.dev2.dist-info → ai_cr-2.0.1.dist-info}/WHEEL +1 -1
- gito/__main__.py +4 -4
- gito/bootstrap.py +66 -66
- gito/cli.py +217 -255
- gito/commands/__init__.py +1 -1
- gito/commands/fix.py +157 -157
- gito/commands/gh_post_review_comment.py +63 -0
- gito/commands/{gh_comment.py → gh_react_to_comment.py} +194 -157
- gito/commands/repl.py +5 -2
- gito/config.toml +453 -415
- gito/constants.py +12 -9
- gito/core.py +288 -239
- gito/gh_api.py +35 -0
- gito/issue_trackers.py +49 -15
- gito/pipeline.py +82 -70
- gito/pipeline_steps/jira.py +57 -83
- gito/pipeline_steps/linear.py +84 -0
- gito/project_config.py +73 -71
- gito/report_struct.py +134 -133
- gito/utils.py +226 -214
- ai_cr-2.0.0.dev2.dist-info/RECORD +0 -23
- {ai_cr-2.0.0.dev2.dist-info → ai_cr-2.0.1.dist-info}/entry_points.txt +0 -0
gito/pipeline.py
CHANGED
@@ -1,70 +1,82 @@
|
|
1
|
-
import logging
|
2
|
-
from enum import StrEnum
|
3
|
-
from dataclasses import dataclass, field
|
4
|
-
|
5
|
-
from gito.utils import is_running_in_github_action
|
6
|
-
from microcore import ui
|
7
|
-
from microcore.utils import resolve_callable
|
8
|
-
|
9
|
-
|
10
|
-
class PipelineEnv(StrEnum):
|
11
|
-
LOCAL = "local"
|
12
|
-
GH_ACTION = "gh-action"
|
13
|
-
|
14
|
-
@staticmethod
|
15
|
-
def all():
|
16
|
-
return [PipelineEnv.LOCAL, PipelineEnv.GH_ACTION]
|
17
|
-
|
18
|
-
@staticmethod
|
19
|
-
def current():
|
20
|
-
return (
|
21
|
-
PipelineEnv.GH_ACTION
|
22
|
-
if is_running_in_github_action()
|
23
|
-
else PipelineEnv.LOCAL
|
24
|
-
)
|
25
|
-
|
26
|
-
|
27
|
-
@dataclass
|
28
|
-
class PipelineStep:
|
29
|
-
call: str
|
30
|
-
envs: list[PipelineEnv] = field(default_factory=PipelineEnv.all)
|
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
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
1
|
+
import logging
|
2
|
+
from enum import StrEnum
|
3
|
+
from dataclasses import dataclass, field
|
4
|
+
|
5
|
+
from gito.utils import is_running_in_github_action
|
6
|
+
from microcore import ui
|
7
|
+
from microcore.utils import resolve_callable
|
8
|
+
|
9
|
+
|
10
|
+
class PipelineEnv(StrEnum):
|
11
|
+
LOCAL = "local"
|
12
|
+
GH_ACTION = "gh-action"
|
13
|
+
|
14
|
+
@staticmethod
|
15
|
+
def all():
|
16
|
+
return [PipelineEnv.LOCAL, PipelineEnv.GH_ACTION]
|
17
|
+
|
18
|
+
@staticmethod
|
19
|
+
def current():
|
20
|
+
return (
|
21
|
+
PipelineEnv.GH_ACTION
|
22
|
+
if is_running_in_github_action()
|
23
|
+
else PipelineEnv.LOCAL
|
24
|
+
)
|
25
|
+
|
26
|
+
|
27
|
+
@dataclass
|
28
|
+
class PipelineStep:
|
29
|
+
call: str
|
30
|
+
envs: list[PipelineEnv] = field(default_factory=PipelineEnv.all)
|
31
|
+
enabled: bool = field(default=True)
|
32
|
+
|
33
|
+
def get_callable(self):
|
34
|
+
"""
|
35
|
+
Resolve the callable from the string representation.
|
36
|
+
"""
|
37
|
+
return resolve_callable(self.call)
|
38
|
+
|
39
|
+
def run(self, *args, **kwargs):
|
40
|
+
return self.get_callable()(*args, **kwargs)
|
41
|
+
|
42
|
+
|
43
|
+
@dataclass
|
44
|
+
class Pipeline:
|
45
|
+
ctx: dict = field(default_factory=dict)
|
46
|
+
steps: dict[str, PipelineStep] = field(default_factory=dict)
|
47
|
+
verbose: bool = False
|
48
|
+
|
49
|
+
@property
|
50
|
+
def enabled_steps(self):
|
51
|
+
return {
|
52
|
+
k: v for k, v in self.steps.items() if v.enabled
|
53
|
+
}
|
54
|
+
|
55
|
+
def run(self, *args, **kwargs):
|
56
|
+
cur_env = PipelineEnv.current()
|
57
|
+
logging.info("Running pipeline... [env: %s]", ui.yellow(cur_env))
|
58
|
+
self.ctx["pipeline_out"] = self.ctx.get("pipeline_out", {})
|
59
|
+
for step_name, step in self.enabled_steps.items():
|
60
|
+
if cur_env in step.envs:
|
61
|
+
logging.info(f"Running pipeline step: {step_name}")
|
62
|
+
try:
|
63
|
+
step_output = step.run(*args, **kwargs, **self.ctx)
|
64
|
+
if isinstance(step_output, dict):
|
65
|
+
self.ctx["pipeline_out"].update(step_output)
|
66
|
+
self.ctx["pipeline_out"][step_name] = step_output
|
67
|
+
if self.verbose and step_output:
|
68
|
+
logging.info(
|
69
|
+
f"Pipeline step {step_name} output: {repr(step_output)}"
|
70
|
+
)
|
71
|
+
if not step_output:
|
72
|
+
logging.warning(
|
73
|
+
f'Pipeline step "{step_name}" returned {repr(step_output)}.'
|
74
|
+
)
|
75
|
+
except Exception as e:
|
76
|
+
logging.error(f'Error in pipeline step "{step_name}": {e}')
|
77
|
+
else:
|
78
|
+
logging.info(
|
79
|
+
f"Skipping pipeline step: {step_name}"
|
80
|
+
f" [env: {ui.yellow(cur_env)} not in {step.envs}]"
|
81
|
+
)
|
82
|
+
return self.ctx["pipeline_out"]
|
gito/pipeline_steps/jira.py
CHANGED
@@ -1,83 +1,57 @@
|
|
1
|
-
import logging
|
2
|
-
import os
|
3
|
-
|
4
|
-
import git
|
5
|
-
from jira import JIRA
|
6
|
-
|
7
|
-
from gito.issue_trackers import
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
if
|
58
|
-
logging.error(f"No Jira issue key found in branch name: {branch_name}")
|
59
|
-
return None
|
60
|
-
|
61
|
-
jira_url = jira_url or os.getenv("JIRA_URL")
|
62
|
-
jira_username = (
|
63
|
-
jira_username
|
64
|
-
or os.getenv("JIRA_USERNAME")
|
65
|
-
or os.getenv("JIRA_USER")
|
66
|
-
or os.getenv("JIRA_EMAIL")
|
67
|
-
)
|
68
|
-
jira_token = (
|
69
|
-
jira_api_token
|
70
|
-
or os.getenv("JIRA_API_TOKEN")
|
71
|
-
or os.getenv("JIRA_API_KEY")
|
72
|
-
or os.getenv("JIRA_TOKEN")
|
73
|
-
)
|
74
|
-
try:
|
75
|
-
assert jira_url, "JIRA_URL is not set"
|
76
|
-
assert jira_username, "JIRA_USERNAME is not set"
|
77
|
-
assert jira_token, "JIRA_API_TOKEN is not set"
|
78
|
-
except AssertionError as e:
|
79
|
-
logging.error(f"Jira configuration error: {e}")
|
80
|
-
return None
|
81
|
-
return dict(
|
82
|
-
associated_issue=fetch_issue(issue_key, jira_url, jira_username, jira_token)
|
83
|
-
)
|
1
|
+
import logging
|
2
|
+
import os
|
3
|
+
|
4
|
+
import git
|
5
|
+
from jira import JIRA
|
6
|
+
|
7
|
+
from gito.issue_trackers import IssueTrackerIssue, resolve_issue_key
|
8
|
+
|
9
|
+
|
10
|
+
def fetch_issue(issue_key, jira_url, username, api_token) -> IssueTrackerIssue | None:
|
11
|
+
try:
|
12
|
+
jira = JIRA(jira_url, basic_auth=(username, api_token))
|
13
|
+
issue = jira.issue(issue_key)
|
14
|
+
return IssueTrackerIssue(
|
15
|
+
title=issue.fields.summary,
|
16
|
+
description=issue.fields.description or "",
|
17
|
+
url=f"{jira_url.rstrip('/')}/browse/{issue_key}"
|
18
|
+
)
|
19
|
+
except Exception as e:
|
20
|
+
logging.error(f"Failed to fetch Jira issue {issue_key}: {e}")
|
21
|
+
return None
|
22
|
+
|
23
|
+
|
24
|
+
def fetch_associated_issue(
|
25
|
+
repo: git.Repo,
|
26
|
+
jira_url=None,
|
27
|
+
jira_username=None,
|
28
|
+
jira_api_token=None,
|
29
|
+
**kwargs
|
30
|
+
):
|
31
|
+
"""
|
32
|
+
Pipeline step to fetch a Jira issue based on the current branch name.
|
33
|
+
"""
|
34
|
+
jira_url = jira_url or os.getenv("JIRA_URL")
|
35
|
+
jira_username = (
|
36
|
+
jira_username
|
37
|
+
or os.getenv("JIRA_USERNAME")
|
38
|
+
or os.getenv("JIRA_USER")
|
39
|
+
or os.getenv("JIRA_EMAIL")
|
40
|
+
)
|
41
|
+
jira_token = (
|
42
|
+
jira_api_token
|
43
|
+
or os.getenv("JIRA_API_TOKEN")
|
44
|
+
or os.getenv("JIRA_API_KEY")
|
45
|
+
or os.getenv("JIRA_TOKEN")
|
46
|
+
)
|
47
|
+
try:
|
48
|
+
assert jira_url, "JIRA_URL is not set"
|
49
|
+
assert jira_username, "JIRA_USERNAME is not set"
|
50
|
+
assert jira_token, "JIRA_API_TOKEN is not set"
|
51
|
+
except AssertionError as e:
|
52
|
+
logging.error(f"Jira configuration error: {e}")
|
53
|
+
return None
|
54
|
+
issue_key = resolve_issue_key(repo)
|
55
|
+
return dict(
|
56
|
+
associated_issue=fetch_issue(issue_key, jira_url, jira_username, jira_token)
|
57
|
+
) if issue_key else None
|
@@ -0,0 +1,84 @@
|
|
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, api_key) -> IssueTrackerIssue | None:
|
11
|
+
"""
|
12
|
+
Fetch a Linear issue using GraphQL API.
|
13
|
+
"""
|
14
|
+
try:
|
15
|
+
url = "https://api.linear.app/graphql"
|
16
|
+
headers = {
|
17
|
+
"Authorization": f"{api_key}",
|
18
|
+
"Content-Type": "application/json"
|
19
|
+
}
|
20
|
+
|
21
|
+
query = """
|
22
|
+
query Issues($teamKey: String!, $issueNumber: Float) {
|
23
|
+
issues(filter: {team: {key: {eq: $teamKey}}, number: {eq: $issueNumber}}) {
|
24
|
+
nodes {
|
25
|
+
id
|
26
|
+
identifier
|
27
|
+
title
|
28
|
+
description
|
29
|
+
url
|
30
|
+
}
|
31
|
+
}
|
32
|
+
}
|
33
|
+
"""
|
34
|
+
team_key, issue_number = issue_key.split("-")
|
35
|
+
response = requests.post(
|
36
|
+
url,
|
37
|
+
json={
|
38
|
+
"query": query,
|
39
|
+
"variables": {'teamKey': team_key, 'issueNumber': int(issue_number)}
|
40
|
+
},
|
41
|
+
headers=headers
|
42
|
+
)
|
43
|
+
response.raise_for_status()
|
44
|
+
data = response.json()
|
45
|
+
|
46
|
+
if "errors" in data:
|
47
|
+
logging.error(f"Linear API error: {data['errors']}")
|
48
|
+
return None
|
49
|
+
|
50
|
+
nodes = data.get("data", {}).get("issues", {}).get("nodes", [])
|
51
|
+
if not nodes:
|
52
|
+
logging.error(f"Linear issue {issue_key} not found")
|
53
|
+
return None
|
54
|
+
|
55
|
+
issue = nodes[0]
|
56
|
+
return IssueTrackerIssue(
|
57
|
+
title=issue["title"],
|
58
|
+
description=issue.get("description") or "",
|
59
|
+
url=issue["url"]
|
60
|
+
)
|
61
|
+
|
62
|
+
except requests.HTTPError as e:
|
63
|
+
logging.error(f"Failed to fetch Linear issue {issue_key}: {e}")
|
64
|
+
logging.error(f"Response body: {response.text}")
|
65
|
+
return None
|
66
|
+
|
67
|
+
|
68
|
+
def fetch_associated_issue(
|
69
|
+
repo: git.Repo,
|
70
|
+
api_key=None,
|
71
|
+
**kwargs
|
72
|
+
):
|
73
|
+
"""
|
74
|
+
Pipeline step to fetch a Linear issue based on the current branch name.
|
75
|
+
"""
|
76
|
+
api_key = api_key or os.getenv("LINEAR_API_KEY")
|
77
|
+
if not api_key:
|
78
|
+
logging.error("LINEAR_API_KEY environment variable is not set")
|
79
|
+
return
|
80
|
+
|
81
|
+
issue_key = resolve_issue_key(repo)
|
82
|
+
return dict(
|
83
|
+
associated_issue=fetch_issue(issue_key, api_key)
|
84
|
+
) if issue_key else None
|
gito/project_config.py
CHANGED
@@ -1,71 +1,73 @@
|
|
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
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
config
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
+
pipeline_steps: dict[str, dict | PipelineStep] = field(default_factory=dict)
|
36
|
+
|
37
|
+
def __post_init__(self):
|
38
|
+
self.pipeline_steps = {
|
39
|
+
k: PipelineStep(**v) if isinstance(v, dict) else v
|
40
|
+
for k, v in self.pipeline_steps.items()
|
41
|
+
}
|
42
|
+
|
43
|
+
@staticmethod
|
44
|
+
def _read_bundled_defaults() -> dict:
|
45
|
+
with open(PROJECT_CONFIG_BUNDLED_DEFAULTS_FILE, "rb") as f:
|
46
|
+
config = tomllib.load(f)
|
47
|
+
return config
|
48
|
+
|
49
|
+
@staticmethod
|
50
|
+
def load_for_repo(repo: Repo):
|
51
|
+
return ProjectConfig.load(Path(repo.working_tree_dir) / PROJECT_CONFIG_FILE_PATH)
|
52
|
+
|
53
|
+
@staticmethod
|
54
|
+
def load(config_path: str | Path | None = None) -> "ProjectConfig":
|
55
|
+
config = ProjectConfig._read_bundled_defaults()
|
56
|
+
github_env = detect_github_env()
|
57
|
+
config["prompt_vars"] |= github_env | dict(github_env=github_env)
|
58
|
+
|
59
|
+
config_path = Path(config_path or PROJECT_CONFIG_FILE_PATH)
|
60
|
+
if config_path.exists():
|
61
|
+
logging.info(
|
62
|
+
f"Loading project-specific configuration from {mc.utils.file_link(config_path)}...")
|
63
|
+
default_prompt_vars = config["prompt_vars"]
|
64
|
+
with open(config_path, "rb") as f:
|
65
|
+
config.update(tomllib.load(f))
|
66
|
+
# overriding prompt_vars config section will not empty default values
|
67
|
+
config["prompt_vars"] = default_prompt_vars | config["prompt_vars"]
|
68
|
+
else:
|
69
|
+
logging.info(
|
70
|
+
f"No project config found at {ui.blue(config_path)}, using defaults"
|
71
|
+
)
|
72
|
+
|
73
|
+
return ProjectConfig(**config)
|