ai-cr 2.0.2__py3-none-any.whl → 3.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.
@@ -1,23 +1,33 @@
1
1
  import logging
2
2
  import os
3
+ from time import sleep
3
4
 
4
5
  import typer
5
- from gito.bootstrap import app
6
- from gito.constants import GITHUB_MD_REPORT_FILE_NAME
7
- from gito.gh_api import post_gh_comment
8
- from gito.project_config import ProjectConfig
6
+ from ghapi.core import GhApi
9
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
10
16
 
11
- @app.command(help="Leave a GitHub PR comment with the review.")
12
- def github_comment(
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"),
13
23
  token: str = typer.Option(
14
- os.environ.get("GITHUB_TOKEN", ""), help="GitHub token (or set GITHUB_TOKEN env var)"
24
+ "", help="GitHub token (or set GITHUB_TOKEN env var)"
15
25
  ),
16
26
  ):
17
27
  """
18
28
  Leaves a comment with the review on the current GitHub pull request.
19
29
  """
20
- file = GITHUB_MD_REPORT_FILE_NAME
30
+ file = md_report_file or GITHUB_MD_REPORT_FILE_NAME
21
31
  if not os.path.exists(file):
22
32
  logging.error(f"Review file not found: {file}, comment will not be posted.")
23
33
  raise typer.Exit(4)
@@ -25,30 +35,31 @@ def github_comment(
25
35
  with open(file, "r", encoding="utf-8") as f:
26
36
  body = f.read()
27
37
 
38
+ token = resolve_gh_token(token)
28
39
  if not token:
29
40
  print("GitHub token is required (--token or GITHUB_TOKEN env var).")
30
41
  raise typer.Exit(1)
31
-
32
- github_env = ProjectConfig.load().prompt_vars["github_env"]
33
- repo = github_env.get("github_repo", "")
34
- pr_env_val = github_env.get("github_pr_number", "")
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", "")
35
46
  logging.info(f"github_pr_number = {pr_env_val}")
36
47
 
37
- pr = None
38
- # e.g. could be "refs/pull/123/merge" or a direct number
39
- if "/" in pr_env_val and "pull" in pr_env_val:
40
- # refs/pull/123/merge
41
- try:
42
- pr_num_candidate = pr_env_val.strip("/").split("/")
43
- idx = pr_num_candidate.index("pull")
44
- pr = int(pr_num_candidate[idx + 1])
45
- except Exception:
46
- pass
47
- else:
48
- try:
49
- pr = int(pr_env_val)
50
- except ValueError:
51
- pass
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
52
63
  if not pr:
53
64
  if pr_str := os.getenv("PR_NUMBER_FROM_WORKFLOW_DISPATCH"):
54
65
  try:
@@ -59,5 +70,42 @@ def github_comment(
59
70
  logging.error("Could not resolve PR number from environment variables.")
60
71
  raise typer.Exit(3)
61
72
 
62
- if not post_gh_comment(repo, pr, token, body):
73
+ if not post_gh_comment(gh_repo, pr, token, body):
63
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.")
@@ -16,10 +16,10 @@ from microcore import ui
16
16
  from ghapi.all import GhApi
17
17
  import git
18
18
 
19
- from ..bootstrap import app
19
+ from ..cli_base import app
20
20
  from ..constants import JSON_REPORT_FILE_NAME, HTML_TEXT_ICON
21
21
  from ..core import answer
22
- from ..gh_api import post_gh_comment
22
+ from ..gh_api import post_gh_comment, resolve_gh_token
23
23
  from ..project_config import ProjectConfig
24
24
  from ..utils import extract_gh_owner_repo
25
25
  from .fix import fix
@@ -51,9 +51,7 @@ def react_to_comment(
51
51
  repo = git.Repo(".") # Current directory
52
52
  owner, repo_name = extract_gh_owner_repo(repo)
53
53
  logging.info(f"Using repository: {ui.yellow}{owner}/{repo_name}{ui.reset}")
54
- gh_token = (
55
- gh_token or os.getenv("GITHUB_TOKEN", None) or os.getenv("GH_TOKEN", None)
56
- )
54
+ gh_token = resolve_gh_token(gh_token)
57
55
  api = GhApi(owner=owner, repo=repo_name, token=gh_token)
58
56
  comment = api.issues.get_comment(comment_id=comment_id)
59
57
  logging.info(
@@ -0,0 +1,53 @@
1
+ import os
2
+ import sys
3
+ import logging
4
+
5
+ import typer
6
+ from git import Repo
7
+
8
+ from ..cli_base import app, arg_refs
9
+ from ..issue_trackers import resolve_issue_key
10
+
11
+ import requests
12
+
13
+
14
+ def post_linear_comment(issue_key, text, api_key):
15
+ response = requests.post(
16
+ 'https://api.linear.app/graphql',
17
+ headers={'Authorization': api_key, 'Content-Type': 'application/json'},
18
+ json={
19
+ 'query': '''
20
+ mutation($issueId: String!, $body: String!) {
21
+ commentCreate(input: {issueId: $issueId, body: $body}) {
22
+ comment { id }
23
+ }
24
+ }
25
+ ''',
26
+ 'variables': {'issueId': issue_key, 'body': text}
27
+ }
28
+ )
29
+ return response.json()
30
+
31
+
32
+ @app.command()
33
+ def linear_comment(
34
+ text: str = typer.Argument(None),
35
+ refs: str = arg_refs(),
36
+ ):
37
+ if text is None or text == "-":
38
+ # Read from stdin if no text provided
39
+ text = sys.stdin.read()
40
+
41
+ if not text or not text.strip():
42
+ typer.echo("Error: No comment text provided.", err=True)
43
+ raise typer.Exit(code=1)
44
+
45
+ api_key = os.getenv("LINEAR_API_KEY")
46
+ if not api_key:
47
+ logging.error("LINEAR_API_KEY environment variable is not set")
48
+ return
49
+
50
+ repo = Repo(".")
51
+ key = resolve_issue_key(repo)
52
+ post_linear_comment(key, text, api_key)
53
+ logging.info("Comment posted to Linear issue %s", key)
gito/commands/repl.py CHANGED
@@ -16,10 +16,11 @@ from rich.pretty import pprint
16
16
  import microcore as mc
17
17
  from microcore import ui
18
18
 
19
- from ..cli import app
19
+ from ..cli_base import app
20
20
  from ..constants import *
21
21
  from ..core import *
22
22
  from ..utils import *
23
+ from ..gh_api import *
23
24
 
24
25
 
25
26
  @app.command(help="python REPL")
gito/config.toml CHANGED
@@ -3,6 +3,7 @@
3
3
  # Defines the keyword or mention tag that triggers bot actions when referenced in code review comments.
4
4
  # list of strings, case-insensitive
5
5
  mention_triggers = ["gito", "bot", "ai", "/fix"]
6
+ collapse_previous_code_review_comments = true
6
7
  report_template_md = """
7
8
  <h2>{{ HTML_TEXT_ICON }}I've Reviewed the Code</h2>
8
9
 
@@ -36,7 +37,7 @@ report_template_md = """
36
37
  {%- endfor -%}
37
38
  {{ "\n" }}
38
39
  {%- endfor -%}
39
-
40
+ {{- HTML_CR_COMMENT_MARKER -}}
40
41
  """
41
42
  report_template_cli = """
42
43
  {{ Back.BLUE }} + + + ---==<<[ CODE REVIEW{{Style.NORMAL}} ]>>==--- + + + {{Style.RESET_ALL}}
@@ -49,7 +50,7 @@ report_template_cli = """
49
50
  {%- if report.summary -%}
50
51
  {{- "\n" }}
51
52
  {{- "\n" }}{{- Style.BRIGHT }}✨ SUMMARY {{ Style.RESET_ALL -}}
52
- {{- "\n" }}{{- report.summary -}}
53
+ {{- "\n" }}{{- remove_html_comments(report.summary) -}}
53
54
  {%- endif %}
54
55
  {% for issue in report.plain_issues -%}
55
56
  {{"\n"}}{{ Style.BRIGHT }}{{Back.RED}}[ {{ issue.id}} ]{{Back.RESET}} {{ issue.title -}}{{ Style.RESET_ALL -}}
@@ -134,18 +135,18 @@ Review the provided code diff carefully and identify *only* highly confident iss
134
135
  {{ requirements -}}
135
136
  {{ json_requirements }}
136
137
 
137
- Respond with a valid JSON array of issues in the following format:
138
+ Respond with a valid JSON array of issues following this schema:
138
139
  [
139
140
  {
140
141
  "title": "<issue_title>",
141
142
  "details": "<issue_description>",
142
- "tags": ["<issue_tag1>", "<issue_tag2>"],
143
- "severity": <issue_severity>,
144
- "confidence": <confidence_score>,
145
- "affected_lines": [ // optional; list of affected lines
143
+ "tags": ["<issue_tag1>", "<issue_tag2>", ...],
144
+ "severity": <issue_severity:int>,
145
+ "confidence": <confidence_score:int>,
146
+ "affected_lines": [ // optional;
146
147
  {
147
- "start_line": <start_line:int>,
148
- "end_line": <end_line:int>,
148
+ "start_line": <int>,
149
+ "end_line": <int>,
149
150
  "proposal": "<proposed code to replace the affected lines (optional)>"
150
151
  },
151
152
  ...
@@ -153,6 +154,10 @@ Respond with a valid JSON array of issues in the following format:
153
154
  },
154
155
  ...
155
156
  ]
157
+
158
+ - if present, `proposal` blocks must match the indentation of the original code
159
+ and apply cleanly to lines `start_line`..`end_line`. It is designed for programmatical substitution.
160
+
156
161
  Available issue tags:
157
162
  - bug
158
163
  - security
@@ -200,14 +205,16 @@ Summarize the code review in one sentence.
200
205
  {{ issues | tojson(indent=2) }}
201
206
  ---
202
207
  If the code changes include exceptional achievements, you may also present an award to the author in the summary text.
203
- Note: Awards should only be given to authors of initial codebase changes, not to code reviewers.
208
+ - (!) Only give awards to initial codebase authors, NOT to reviewers.
209
+ - (!) If you give an award, place the hidden <!-- award --> HTML comment on its own line immediately before the award text.
204
210
  --Available Awards--
205
211
  {{ awards }}
206
212
  ---
207
213
  {% if pipeline_out.associated_issue and pipeline_out.associated_issue.title %}
208
214
  ----SUBTASK----
209
215
  Include one sentence about how the code changes address the requirements of the associated issue listed below.
210
-
216
+ - (!) Place the hidden <!-- issue_alignment --> comment on its own line immediately before the related text.
217
+ - Use ✅ or ⚠️ to indicate whether the implementation fully satisfies the issue requirements.
211
218
  --Associated Issue--
212
219
  # {{ pipeline_out.associated_issue.title }}
213
220
  {{ pipeline_out.associated_issue.description }}
@@ -218,10 +225,12 @@ Examples:
218
225
 
219
226
  If the implementation fully delivers the requested functionality:
220
227
  ```
228
+ <!-- issue_alignment -->
221
229
  ✅ Implementation Satisfies [<ISSUE_KEY>](<ISSUE_URL>).
222
230
  ```
223
231
  If there are concerns about how thoroughly the code covers the requirements and technical description from the associated issue:
224
232
  ```
233
+ <!-- issue_alignment -->
225
234
  ⚠️ <Describe specific gap or concern>.
226
235
  ⚠️ <Describe additional limitation or missing feature>.
227
236
  ```
@@ -248,6 +257,14 @@ Answer the following user question:
248
257
  --FILE: {{ file }}--
249
258
  {{ file_lines }}
250
259
  {% endfor %}
260
+
261
+ {%- if pipeline_out.associated_issue and pipeline_out.associated_issue.title %}
262
+ ----ASSOCIATED ISSUE----
263
+ # {{ pipeline_out.associated_issue.title }}
264
+ {{ pipeline_out.associated_issue.description }}
265
+ URL: {{ pipeline_out.associated_issue.url }}
266
+ {%- endif -%}{{ '\n' }}
267
+
251
268
  ----ANSWERING INSTRUCTIONS----
252
269
  {{ answering_instructions }}
253
270
  """
@@ -433,7 +450,9 @@ decorators add depth and texture, and observer masterfully completes the composi
433
450
  The Gang of Four gives a standing ovation from the stalls."
434
451
  ```
435
452
  """
436
- requirements = ""
453
+ requirements = """
454
+ - (!) Never report issues related to software versions, model names, or similar details that you believe have not yet been released—you cannot reliably determine this.
455
+ """
437
456
  summary_requirements = ""
438
457
  answering_instructions = """
439
458
  - (!) Provide a concise, direct answer in engaging speech.
gito/constants.py CHANGED
@@ -10,3 +10,4 @@ GITHUB_MD_REPORT_FILE_NAME = "code-review-report.md"
10
10
  EXECUTABLE = "gito"
11
11
  TEXT_ICON_URL = 'https://raw.githubusercontent.com/Nayjest/Gito/main/press-kit/logo/gito-bot-1_64top.png' # noqa: E501
12
12
  HTML_TEXT_ICON = f'<a href="https://github.com/Nayjest/Gito"><img src="{TEXT_ICON_URL}" align="left" width=64 height=50 /></a>' # noqa: E501
13
+ HTML_CR_COMMENT_MARKER = '<!-- GITO_COMMENT:CODE_REVIEW_REPORT -->'
gito/context.py ADDED
@@ -0,0 +1,19 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Iterable, TYPE_CHECKING
3
+
4
+ from unidiff.patch import PatchSet, PatchedFile
5
+ from git import Repo
6
+
7
+
8
+ if TYPE_CHECKING:
9
+ from .project_config import ProjectConfig
10
+ from .report_struct import Report
11
+
12
+
13
+ @dataclass
14
+ class Context:
15
+ report: "Report"
16
+ config: "ProjectConfig"
17
+ diff: PatchSet | Iterable[PatchedFile]
18
+ repo: Repo
19
+ pipeline_out: dict = field(default_factory=dict)
gito/core.py CHANGED
@@ -3,17 +3,22 @@ import logging
3
3
  from os import PathLike
4
4
  from typing import Iterable
5
5
  from pathlib import Path
6
+ from functools import partial
6
7
 
7
8
  import microcore as mc
8
- from git import Repo
9
- from gito.pipeline import Pipeline
9
+ from microcore import ui
10
+ from git import Repo, Commit
11
+ from git.exc import GitCommandError
10
12
  from unidiff import PatchSet, PatchedFile
11
13
  from unidiff.constants import DEV_NULL
12
14
 
15
+ from .context import Context
13
16
  from .project_config import ProjectConfig
14
17
  from .report_struct import Report
15
18
  from .constants import JSON_REPORT_FILE_NAME
16
- from .utils import stream_to_cli
19
+ from .utils import make_streaming_function
20
+ from .pipeline import Pipeline
21
+ from .env import Env
17
22
 
18
23
 
19
24
  def review_subject_is_index(what):
@@ -51,6 +56,16 @@ def is_binary_file(repo: Repo, file_path: str) -> bool:
51
56
  return True # Conservatively treat errors as binary to avoid issues
52
57
 
53
58
 
59
+ def commit_in_branch(repo: Repo, commit: Commit, target_branch: str) -> bool:
60
+ try:
61
+ # exit code 0 if commit is ancestor of branch
62
+ repo.git.merge_base('--is-ancestor', commit.hexsha, target_branch)
63
+ return True
64
+ except GitCommandError:
65
+ pass
66
+ return False
67
+
68
+
54
69
  def get_diff(
55
70
  repo: Repo = None,
56
71
  what: str = None,
@@ -76,12 +91,75 @@ def get_diff(
76
91
  else:
77
92
  current_ref = what
78
93
  merge_base = repo.merge_base(current_ref or repo.active_branch.name, against)[0]
79
- against = merge_base.hexsha
80
94
  logging.info(
81
- f"Using merge base: {mc.ui.cyan(merge_base.hexsha[:8])} ({merge_base.summary})"
95
+ f"Merge base({ui.green(current_ref)},{ui.yellow(against)})"
96
+ f" --> {ui.cyan(merge_base.hexsha)}"
82
97
  )
98
+ # if branch is already an ancestor of "against", merge_base == branch ⇒ it’s been merged
99
+ if merge_base.hexsha == repo.commit(current_ref or repo.active_branch.name).hexsha:
100
+ # @todo: check case: reviewing working copy index in main branch #103
101
+ logging.info(
102
+ f"Branch is already merged. ({ui.green(current_ref)} vs {ui.yellow(against)})"
103
+ )
104
+ merge_sha = repo.git.log(
105
+ '--merges',
106
+ '--ancestry-path',
107
+ f'{current_ref}..{against}',
108
+ '-n',
109
+ '1',
110
+ '--pretty=format:%H'
111
+ ).strip()
112
+ if merge_sha:
113
+ logging.info(f"Merge commit is {ui.cyan(merge_sha)}")
114
+ merge_commit = repo.commit(merge_sha)
115
+
116
+ other_merge_parent = None
117
+ for parent in merge_commit.parents:
118
+ logging.info(f"Checking merge parent: {parent.hexsha[:8]}")
119
+ if parent.hexsha == merge_base.hexsha:
120
+ logging.info(f"merge parent is {ui.cyan(parent.hexsha[:8])}, skipping")
121
+ continue
122
+ if not commit_in_branch(repo, parent, against):
123
+ logging.warning(f"merge parent is not in {against}, skipping")
124
+ continue
125
+ logging.info(f"Found other merge parent: {ui.cyan(parent.hexsha[:8])}")
126
+ other_merge_parent = parent
127
+ break
128
+ if other_merge_parent:
129
+ first_common_ancestor = repo.merge_base(other_merge_parent, merge_base)[0]
130
+ # for gito remote (feature_branch vs origin/main)
131
+ # the same merge base appears in first_common_ancestor again
132
+ if first_common_ancestor.hexsha == merge_base.hexsha:
133
+ if merge_base.parents:
134
+ first_common_ancestor = repo.merge_base(
135
+ other_merge_parent, merge_base.parents[0]
136
+ )[0]
137
+ else:
138
+ logging.error(
139
+ "merge_base has no parents, "
140
+ "using merge_base as first_common_ancestor"
141
+ )
142
+ logging.info(
143
+ f"{what} will be compared to "
144
+ f"first common ancestor of {what} and {against}: "
145
+ f"{ui.cyan(first_common_ancestor.hexsha[:8])}"
146
+ )
147
+ against = first_common_ancestor.hexsha
148
+ else:
149
+ logging.error(f"Can't find other merge parent for {merge_sha}")
150
+ else:
151
+ logging.error(
152
+ f"No merge‐commit found for {current_ref!r}→{against!r}; "
153
+ "falling back to merge‐base diff"
154
+ )
155
+ else:
156
+ # normal case: branch not yet merged
157
+ against = merge_base.hexsha
158
+ logging.info(
159
+ f"Using merge base: {ui.cyan(merge_base.hexsha[:8])} ({merge_base.summary})"
160
+ )
83
161
  logging.info(
84
- f"Making diff: {mc.ui.green(what or 'INDEX')} vs {mc.ui.yellow(against)}"
162
+ f"Making diff: {ui.green(what or 'INDEX')} vs {ui.yellow(against)}"
85
163
  )
86
164
  diff_content = repo.git.diff(against, what)
87
165
  diff = PatchSet.from_string(diff_content)
@@ -145,16 +223,17 @@ def file_lines(repo: Repo, file: str, max_tokens: int = None, use_local_files: b
145
223
  return "".join(lines)
146
224
 
147
225
 
148
- def make_cr_summary(config: ProjectConfig, report: Report, diff, **kwargs) -> str:
226
+ def make_cr_summary(ctx: Context, **kwargs) -> str:
149
227
  return (
150
228
  mc.prompt(
151
- config.summary_prompt,
152
- diff=mc.tokenizing.fit_to_token_size(diff, config.max_code_tokens)[0],
153
- issues=report.issues,
154
- **config.prompt_vars,
229
+ ctx.config.summary_prompt,
230
+ diff=mc.tokenizing.fit_to_token_size(ctx.diff, ctx.config.max_code_tokens)[0],
231
+ issues=ctx.report.issues,
232
+ pipeline_out=ctx.pipeline_out,
233
+ **ctx.config.prompt_vars,
155
234
  **kwargs,
156
235
  ).to_llm()
157
- if config.summary_prompt
236
+ if ctx.config.summary_prompt
158
237
  else ""
159
238
  )
160
239
 
@@ -197,6 +276,41 @@ def _prepare(
197
276
  return repo, cfg, diff, lines
198
277
 
199
278
 
279
+ def get_affected_code_block(repo: Repo, file: str, start_line: int, end_line: int) -> str | None:
280
+ if not start_line or not end_line:
281
+ return None
282
+ try:
283
+ if isinstance(start_line, str):
284
+ start_line = int(start_line)
285
+ if isinstance(end_line, str):
286
+ end_line = int(end_line)
287
+ lines = file_lines(repo, file, max_tokens=None, use_local_files=True)
288
+ if lines:
289
+ lines = [""] + lines.splitlines()
290
+ return "\n".join(
291
+ lines[start_line: end_line + 1]
292
+ )
293
+ except Exception as e:
294
+ logging.error(
295
+ f"Error getting affected code block for {file} from {start_line} to {end_line}: {e}"
296
+ )
297
+ return None
298
+
299
+
300
+ def provide_affected_code_blocks(issues: dict, repo: Repo):
301
+ for file, file_issues in issues.items():
302
+ for issue in file_issues:
303
+ for i in issue.get("affected_lines", []):
304
+ file_name = i.get("file", issue.get("file", file))
305
+ if block := get_affected_code_block(
306
+ repo,
307
+ file_name,
308
+ i.get("start_line"),
309
+ i.get("end_line")
310
+ ):
311
+ i["affected_code"] = block
312
+
313
+
200
314
  async def review(
201
315
  repo: Repo = None,
202
316
  what: str = None,
@@ -226,24 +340,16 @@ async def review(
226
340
  parse_json=True,
227
341
  )
228
342
  issues = {file.path: issues for file, issues in zip(diff, responses) if issues}
229
- for file, file_issues in issues.items():
230
- for issue in file_issues:
231
- for i in issue.get("affected_lines", []):
232
- if lines[file]:
233
- f_lines = [""] + lines[file].splitlines()
234
- i["affected_code"] = "\n".join(
235
- f_lines[i["start_line"]: i["end_line"] + 1]
236
- )
343
+ provide_affected_code_blocks(issues, repo)
237
344
  exec(cfg.post_process, {"mc": mc, **locals()})
238
345
  out_folder = Path(out_folder or repo.working_tree_dir)
239
346
  out_folder.mkdir(parents=True, exist_ok=True)
240
347
  report = Report(issues=issues, number_of_processed_files=len(diff))
241
- ctx = dict(
348
+ ctx = Context(
242
349
  report=report,
243
350
  config=cfg,
244
351
  diff=diff,
245
352
  repo=repo,
246
- pipeline_out={},
247
353
  )
248
354
  if cfg.pipeline_steps:
249
355
  pipe = Pipeline(
@@ -254,7 +360,7 @@ async def review(
254
360
  else:
255
361
  logging.info("No pipeline steps defined, skipping pipeline execution")
256
362
 
257
- report.summary = make_cr_summary(**ctx)
363
+ report.summary = make_cr_summary(ctx)
258
364
  report.save(file_name=out_folder / JSON_REPORT_FILE_NAME)
259
365
  report_text = report.render(cfg, Report.Format.MARKDOWN)
260
366
  text_report_path = out_folder / "code-review-report.md"
@@ -269,20 +375,41 @@ def answer(
269
375
  against: str = None,
270
376
  filters: str | list[str] = "",
271
377
  use_merge_base: bool = True,
378
+ use_pipeline: bool = True,
379
+ prompt_file: str = None,
272
380
  ) -> str | None:
273
381
  try:
274
- repo, cfg, diff, lines = _prepare(
382
+ repo, config, diff, lines = _prepare(
275
383
  repo=repo, what=what, against=against, filters=filters, use_merge_base=use_merge_base
276
384
  )
277
385
  except NoChangesInContextError:
278
386
  logging.error("No changes to review")
279
387
  return
280
- response = mc.llm(mc.prompt(
281
- cfg.answer_prompt,
282
- question=question,
388
+
389
+ ctx = Context(
390
+ repo=repo,
283
391
  diff=diff,
284
- all_file_lines=lines,
285
- **cfg.prompt_vars,
286
- callback=stream_to_cli
287
- ))
392
+ config=config,
393
+ report=Report()
394
+ )
395
+ if use_pipeline:
396
+ pipe = Pipeline(
397
+ ctx=ctx,
398
+ steps=config.pipeline_steps
399
+ )
400
+ pipe.run()
401
+ if prompt_file:
402
+ prompt_func = partial(mc.tpl, prompt_file)
403
+ else:
404
+ prompt_func = partial(mc.prompt, config.answer_prompt)
405
+ response = mc.llm(
406
+ prompt_func(
407
+ question=question,
408
+ diff=diff,
409
+ all_file_lines=lines,
410
+ pipeline_out=ctx.pipeline_out,
411
+ **config.prompt_vars,
412
+ ),
413
+ callback=make_streaming_function() if Env.verbosity == 0 else None,
414
+ )
288
415
  return response
gito/env.py ADDED
@@ -0,0 +1,3 @@
1
+ class Env:
2
+ logging_level: int = 1
3
+ verbosity: int = 1