ai-cr 2.0.3__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,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ai-cr
3
- Version: 2.0.3
3
+ Version: 3.0.0
4
4
  Summary: AI code review tool that works with any language model provider. It detects issues in GitHub pull requests or local changes—instantly, reliably, and without vendor lock-in.
5
5
  License: MIT
6
6
  Keywords: static code analysis,code review,code quality,ai,coding,assistant,llm,github,automation,devops,developer tools,github actions,workflows,git
@@ -16,8 +16,8 @@ Classifier: Programming Language :: Python :: 3.12
16
16
  Classifier: Programming Language :: Python :: 3.13
17
17
  Classifier: Topic :: Software Development
18
18
  Requires-Dist: GitPython (>=3.1.44,<4.0.0)
19
- Requires-Dist: ai-microcore (==4.0.0)
20
- Requires-Dist: anthropic (>=0.52.2,<0.53.0)
19
+ Requires-Dist: ai-microcore (==4.2.1)
20
+ Requires-Dist: anthropic (>=0.57.1,<0.58.0)
21
21
  Requires-Dist: ghapi (>=1.0.6,<1.1.0)
22
22
  Requires-Dist: google-generativeai (>=0.8.5,<0.9.0)
23
23
  Requires-Dist: jira (>=3.8.0,<4.0.0)
@@ -87,7 +87,7 @@ jobs:
87
87
  uses: actions/setup-python@v5
88
88
  with: { python-version: "3.13" }
89
89
  - name: Install AI Code Review tool
90
- run: pip install gito.bot~=2.0
90
+ run: pip install gito.bot~=3.0
91
91
  - name: Run AI code analysis
92
92
  env:
93
93
  LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
@@ -0,0 +1,36 @@
1
+ gito/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ gito/__main__.py,sha256=MSmt_5Xg84uHqzTN38JwgseJK8rsJn_11A8WD99VtEo,61
3
+ gito/bootstrap.py,sha256=9MCV80dMs8QFaB0tmuSlrpPbnRu2ACPHAWKnOSQV-xI,3168
4
+ gito/cli.py,sha256=H6LpoUfw-R4BdJcG4WGcktaIlq2C1kgKWIZ7IEwFljI,6808
5
+ gito/cli_base.py,sha256=h0CvkfOaIYGArqNuKyAuDcJDx-hOB7H7uyfJV3afINg,2390
6
+ gito/commands/__init__.py,sha256=B2uUQsLMEsHfNT1N3lWYm38WSuQIHFmjiGs2tdBuDBA,55
7
+ gito/commands/deploy.py,sha256=PbNcw7Ccau2Bncpi121YDRz9gg9xvkrviytrAEFfpmw,3749
8
+ gito/commands/fix.py,sha256=C4imdS776yl7g-KmH9Wu2PqeN4WNJh8NvSeV3pV-63g,5304
9
+ gito/commands/gh_post_review_comment.py,sha256=xrCauuifrUEufBjx43sw5FMtWP9seHiGYzwAq5SFnvQ,3795
10
+ gito/commands/gh_react_to_comment.py,sha256=3R0Ke-Met06epooImI6ZC2TuLQ2Qpv276OWMSelVcNY,6395
11
+ gito/commands/linear_comment.py,sha256=8kCudz0cogWm8LsgLRWN-BybDq13zEqwV456JB3_7xo,1411
12
+ gito/commands/repl.py,sha256=jJCRO-O7r_fFXZrk9f1_oAcYPOAwPi3cGI3bMoRyer8,549
13
+ gito/config.toml,sha256=kd2fbztnr2LGaNE2BE81yt1Ed0PTF5xe_8WtTgDxxkw,18417
14
+ gito/constants.py,sha256=_G40SAMfuYjYsBzEVUIxKT8Vo0dAWIQqCWWxdaBAltY,766
15
+ gito/context.py,sha256=OBfcQOREsNx8WHANsplNrnrKYrXz1PyZyne11lSfZjw,446
16
+ gito/core.py,sha256=Huz0WnnuuojwFWKD_zPLIjB6wzcoGw3r7a4zzDiQLBI,14484
17
+ gito/env.py,sha256=HWNQb2q1mzyTg6w1FmDIxUheNSuoIUEsIEPiC1SYaYY,61
18
+ gito/gh_api.py,sha256=FbJ7w6dtuAFUVbZNyNHn8RRDGBvJyQlrIX82m46cTac,2958
19
+ gito/issue_trackers.py,sha256=XYspyaIuf0ANQSvDUea5_oOdo9tQvpZZsapI5S9g78U,1551
20
+ gito/pipeline.py,sha256=H6eiyDHUg_p3jxNVhSnyB7EVVNbMIZkA35WMymgPnh0,2610
21
+ gito/pipeline_steps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
+ gito/pipeline_steps/jira.py,sha256=NjFgpGFAkT5PSXi6jQ9Yr8CY9M8sa3_rMb7RFhdpoNg,1862
23
+ gito/pipeline_steps/linear.py,sha256=6UDc8nGKGpwHruPq8VItE2QBWshWxaTapoMhu_qjN_g,2445
24
+ gito/project_config.py,sha256=jOPVMgN01_krELlNXzZlrZOKmXZP-RLDS7iOgVSUyic,3182
25
+ gito/report_struct.py,sha256=96gDYnw0MXhOZrrGaNOTyWstqpjjS7_cuWufd0XJzR4,4320
26
+ gito/tpl/github_workflows/components/env-vars.j2,sha256=ouZY4vpKUkAAM5qUxA3BKAh9BpJMfwsrn8qqahhrh7U,356
27
+ gito/tpl/github_workflows/components/installs.j2,sha256=j5wl0yVEIrXZDpAgzqBwmhXQA9End3xFspPxr2ZzHR0,693
28
+ gito/tpl/github_workflows/gito-code-review.yml.j2,sha256=PcxXMTblWX2ANK-8bkxFPI3H-xby5Do_yaufhxGzw0Y,1159
29
+ gito/tpl/github_workflows/gito-react-to-comments.yml.j2,sha256=zVNkJRgje75AejXAePZugruDu3pOuDaPcujrDfth8K4,2165
30
+ gito/tpl/release_notes.j2,sha256=20QtQwdZYcFEjmfYxTysenY-njTPl2nmHpv9077WFdg,849
31
+ gito/utils.py,sha256=OSBn7IeWKjoLJoGOcdgQcJHBA69UiHUChtaYMInUeko,6852
32
+ ai_cr-3.0.0.dist-info/LICENSE,sha256=VbdF_GbbDK24JvdTfnsxa2M6jmhsxmRSFeHCx-lICGE,1075
33
+ ai_cr-3.0.0.dist-info/METADATA,sha256=HzNuuuJhSDq9LwSRuzx0QUOHQRhC7-p3KX6XmAoYH_c,7989
34
+ ai_cr-3.0.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
35
+ ai_cr-3.0.0.dist-info/entry_points.txt,sha256=Ua1DxkhJJ8TZuLgnH-IlWCkrre_0S0dq_GtYRaYupWk,38
36
+ ai_cr-3.0.0.dist-info/RECORD,,
gito/bootstrap.py CHANGED
@@ -1,16 +1,18 @@
1
- import logging
2
1
  import os
2
+ import sys
3
+ import io
4
+ import logging
3
5
  from datetime import datetime
4
6
  from pathlib import Path
5
7
 
6
8
  import microcore as mc
7
- import typer
8
9
 
9
10
  from .utils import is_running_in_github_action
10
11
  from .constants import HOME_ENV_PATH, EXECUTABLE, PROJECT_GITO_FOLDER
12
+ from .env import Env
11
13
 
12
14
 
13
- def setup_logging():
15
+ def setup_logging(log_level: int = logging.INFO):
14
16
  class CustomFormatter(logging.Formatter):
15
17
  def format(self, record):
16
18
  dt = datetime.fromtimestamp(record.created).strftime("%Y-%m-%d %H:%M:%S")
@@ -25,23 +27,41 @@ def setup_logging():
25
27
 
26
28
  handler = logging.StreamHandler()
27
29
  handler.setFormatter(CustomFormatter())
28
- logging.basicConfig(level=logging.INFO, handlers=[handler])
30
+ logging.basicConfig(level=log_level, handlers=[handler])
29
31
 
30
32
 
31
- def bootstrap():
33
+ def bootstrap(verbosity: int = 1):
32
34
  """Bootstrap the application with the environment configuration."""
33
- setup_logging()
34
- logging.info("Bootstrapping...")
35
+ log_levels_by_verbosity = {
36
+ 0: logging.CRITICAL,
37
+ 1: logging.INFO,
38
+ 2: logging.DEBUG,
39
+ 3: logging.DEBUG,
40
+ }
41
+ Env.verbosity = verbosity
42
+ Env.logging_level = log_levels_by_verbosity.get(verbosity, logging.INFO)
43
+ setup_logging(Env.logging_level)
44
+ logging.info("Bootstrapping... "+mc.ui.gray(f"[verbosity={verbosity}]"))
45
+
46
+ # cp1251 is used on Windows when redirecting output
47
+ if sys.stdout.encoding and sys.stdout.encoding.lower() != "utf-8":
48
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
49
+
35
50
  try:
36
51
  mc.configure(
37
52
  DOT_ENV_FILE=HOME_ENV_PATH,
38
- USE_LOGGING=True,
53
+ USE_LOGGING=verbosity >= 1,
39
54
  EMBEDDING_DB_TYPE=mc.EmbeddingDbType.NONE,
40
55
  PROMPT_TEMPLATES_PATH=[
41
56
  PROJECT_GITO_FOLDER,
42
57
  Path(__file__).parent / "tpl"
43
58
  ],
44
59
  )
60
+ if verbosity > 1:
61
+ mc.logging.LoggingConfig.STRIP_REQUEST_LINES = None
62
+ else:
63
+ mc.logging.LoggingConfig.STRIP_REQUEST_LINES = [300, 15]
64
+
45
65
  except mc.LLMConfigError as e:
46
66
  msg = str(e)
47
67
  if is_running_in_github_action():
@@ -65,7 +85,3 @@ def bootstrap():
65
85
  except Exception as e:
66
86
  logging.error(f"Unexpected configuration error: {e}")
67
87
  raise SystemExit(3)
68
- mc.logging.LoggingConfig.STRIP_REQUEST_LINES = [300, 15]
69
-
70
-
71
- app = typer.Typer(pretty_exceptions_show_locals=False)
gito/cli.py CHANGED
@@ -1,22 +1,34 @@
1
+ import os
1
2
  import asyncio
2
3
  import logging
3
4
  import sys
4
5
  import textwrap
5
- import tempfile
6
6
 
7
7
  import microcore as mc
8
8
  import typer
9
9
  from git import Repo
10
10
 
11
11
  from .core import review, get_diff, filter_diff, answer
12
+ from .cli_base import (
13
+ app,
14
+ args_to_target,
15
+ arg_refs,
16
+ arg_what,
17
+ arg_filters,
18
+ arg_out,
19
+ arg_against,
20
+ get_repo_context,
21
+ )
12
22
  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
23
+ from .constants import HOME_ENV_PATH, GITHUB_MD_REPORT_FILE_NAME
24
+ from .bootstrap import bootstrap
25
+ from .utils import no_subcommand, extract_gh_owner_repo, remove_html_comments
26
+ from .gh_api import resolve_gh_token
16
27
 
17
28
  # Import fix command to register it
18
- from .commands import fix, gh_post_review_comment, gh_react_to_comment, repl, deploy # noqa
19
-
29
+ from .commands import fix, gh_react_to_comment, repl, deploy # noqa
30
+ from .commands.gh_post_review_comment import post_github_cr_comment
31
+ from .commands.linear_comment import linear_comment
20
32
 
21
33
  app_no_subcommand = typer.Typer(pretty_exceptions_show_locals=False)
22
34
 
@@ -36,67 +48,26 @@ def main():
36
48
 
37
49
 
38
50
  @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(
51
+ def cli(
52
+ ctx: typer.Context,
53
+ verbose: bool = typer.Option(default=None),
54
+ verbosity: int = typer.Option(
88
55
  None,
89
- "--out", "-o", "--output",
90
- help="Output folder for the code review report"
91
- )
92
-
56
+ '--verbosity', '-v',
57
+ help="Set verbosity level (0-3, default 1)"
58
+ ),
59
+ ):
60
+ if verbose is not None and verbosity is not None:
61
+ raise typer.BadParameter(
62
+ "Please specify either --verbose or --verbosity, not both."
63
+ )
64
+ if verbose is not None:
65
+ verbosity = 2 if verbose else 0
66
+ if verbosity is None:
67
+ verbosity = 1
93
68
 
94
- def arg_against() -> typer.Option:
95
- return typer.Option(
96
- None,
97
- "--against", "-vs", "--vs",
98
- help="Git ref to compare against"
99
- )
69
+ if ctx.invoked_subcommand != "setup":
70
+ bootstrap(verbosity)
100
71
 
101
72
 
102
73
  @app_no_subcommand.command(name="review", help="Perform code review")
@@ -108,16 +79,43 @@ def cmd_review(
108
79
  against: str = arg_against(),
109
80
  filters: str = arg_filters(),
110
81
  merge_base: bool = typer.Option(default=True, help="Use merge base for comparison"),
82
+ url: str = typer.Option("", "--url", help="Git repository URL"),
83
+ post_comment: bool = typer.Option(default=False, help="Post review comment to GitHub"),
84
+ pr: int = typer.Option(
85
+ default=None,
86
+ help=textwrap.dedent("""\n
87
+ GitHub Pull Request number to post the comment to
88
+ (for local usage together with --post-comment,
89
+ in the github actions PR is resolved from the environment)
90
+ """)
91
+ ),
111
92
  out: str = arg_out()
112
93
  ):
113
94
  _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
- ))
95
+ with get_repo_context(url, _what) as (repo, out_folder):
96
+ asyncio.run(review(
97
+ repo=repo,
98
+ what=_what,
99
+ against=_against,
100
+ filters=filters,
101
+ use_merge_base=merge_base,
102
+ out_folder=out or out_folder,
103
+ ))
104
+ if post_comment:
105
+ try:
106
+ owner, repo_name = extract_gh_owner_repo(repo)
107
+ except ValueError as e:
108
+ logging.error(
109
+ "Error posting comment:\n"
110
+ "Could not extract GitHub owner and repository name from the local repository."
111
+ )
112
+ raise typer.Exit(code=1) from e
113
+ post_github_cr_comment(
114
+ md_report_file=os.path.join(out or out_folder, GITHUB_MD_REPORT_FILE_NAME),
115
+ pr=pr,
116
+ gh_repo=f"{owner}/{repo_name}",
117
+ token=resolve_gh_token()
118
+ )
121
119
 
122
120
 
123
121
  @app.command(name="ask", help="Answer questions about codebase changes")
@@ -130,15 +128,32 @@ def cmd_answer(
130
128
  against: str = arg_against(),
131
129
  filters: str = arg_filters(),
132
130
  merge_base: bool = typer.Option(default=True, help="Use merge base for comparison"),
131
+ use_pipeline: bool = typer.Option(default=True),
132
+ post_to: str = typer.Option(
133
+ help="Post answer to ... Supported values: linear",
134
+ default=None,
135
+ show_default=False
136
+ ),
133
137
  ):
134
138
  _what, _against = args_to_target(refs, what, against)
135
- return answer(
139
+ if str(question).startswith("tpl:"):
140
+ prompt_file = str(question)[4:]
141
+ question = ""
142
+ else:
143
+ prompt_file = None
144
+ out = answer(
136
145
  question=question,
137
146
  what=_what,
138
147
  against=_against,
139
148
  filters=filters,
140
149
  use_merge_base=merge_base,
150
+ prompt_file=prompt_file,
151
+ use_pipeline=use_pipeline,
141
152
  )
153
+ if post_to == 'linear':
154
+ logging.info("Posting answer to Linear...")
155
+ linear_comment(remove_html_comments(out))
156
+ return out
142
157
 
143
158
 
144
159
  @app.command(help="Configure LLM for local usage interactively")
@@ -160,31 +175,6 @@ def render(
160
175
  Report.load(file_name=source).to_cli(report_format=format)
161
176
 
162
177
 
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
178
  @app.command(help="List files in the diff. Might be useful to check what will be reviewed.")
189
179
  def files(
190
180
  refs: str = arg_refs(),
@@ -196,22 +186,25 @@ def files(
196
186
  ):
197
187
  _what, _against = args_to_target(refs, what, against)
198
188
  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), " ")))
189
+ try:
190
+ patch_set = get_diff(repo=repo, what=_what, against=_against, use_merge_base=merge_base)
191
+ patch_set = filter_diff(patch_set, filters)
192
+ print(
193
+ f"Changed files: "
194
+ f"{mc.ui.green(_what or 'INDEX')} vs "
195
+ f"{mc.ui.yellow(_against or repo.remotes.origin.refs.HEAD.reference.name)}"
196
+ f"{' filtered by ' + mc.ui.cyan(filters) if filters else ''}"
197
+ )
198
+
199
+ for patch in patch_set:
200
+ if patch.is_added_file:
201
+ color = mc.ui.green
202
+ elif patch.is_removed_file:
203
+ color = mc.ui.red
204
+ else:
205
+ color = mc.ui.blue
206
+ print(f"- {color(patch.path)}")
207
+ if diff:
208
+ print(mc.ui.gray(textwrap.indent(str(patch), " ")))
209
+ finally:
210
+ repo.close()
gito/cli_base.py ADDED
@@ -0,0 +1,90 @@
1
+ import contextlib
2
+ import logging
3
+ import tempfile
4
+
5
+ import microcore as mc
6
+ import typer
7
+ from git import Repo
8
+ from gito.utils import parse_refs_pair
9
+
10
+
11
+ def args_to_target(refs, what, against) -> tuple[str | None, str | None]:
12
+ _what, _against = parse_refs_pair(refs)
13
+ if _what:
14
+ if what:
15
+ raise typer.BadParameter(
16
+ "You cannot specify both 'refs' <WHAT>..<AGAINST> and '--what'. Use one of them."
17
+ )
18
+ else:
19
+ _what = what
20
+ if _against:
21
+ if against:
22
+ raise typer.BadParameter(
23
+ "You cannot specify both 'refs' <WHAT>..<AGAINST> and '--against'. Use one of them."
24
+ )
25
+ else:
26
+ _against = against
27
+ return _what, _against
28
+
29
+
30
+ def arg_refs() -> typer.Argument:
31
+ return typer.Argument(
32
+ default=None,
33
+ help="Git refs to review, [what]..[against] e.g. 'HEAD..HEAD~1'"
34
+ )
35
+
36
+
37
+ def arg_what() -> typer.Option:
38
+ return typer.Option(None, "--what", "-w", help="Git ref to review")
39
+
40
+
41
+ def arg_filters() -> typer.Option:
42
+ return typer.Option(
43
+ "", "--filter", "-f", "--filters",
44
+ help="""
45
+ filter reviewed files by glob / fnmatch pattern(s),
46
+ e.g. 'src/**/*.py', may be comma-separated
47
+ """,
48
+ )
49
+
50
+
51
+ def arg_out() -> typer.Option:
52
+ return typer.Option(
53
+ None,
54
+ "--out", "-o", "--output",
55
+ help="Output folder for the code review report"
56
+ )
57
+
58
+
59
+ def arg_against() -> typer.Option:
60
+ return typer.Option(
61
+ None,
62
+ "--against", "-vs", "--vs",
63
+ help="Git ref to compare against"
64
+ )
65
+
66
+
67
+ app = typer.Typer(pretty_exceptions_show_locals=False)
68
+
69
+
70
+ @contextlib.contextmanager
71
+ def get_repo_context(url: str, branch: str):
72
+ """Context manager for handling both local and remote repositories."""
73
+ if url:
74
+ with tempfile.TemporaryDirectory() as temp_dir:
75
+ logging.info(
76
+ f"get_repo_context: "
77
+ f"Cloning [{mc.ui.green(url)}] to {mc.utils.file_link(temp_dir)} ..."
78
+ )
79
+ repo = Repo.clone_from(url, branch=branch, to_path=temp_dir)
80
+ try:
81
+ yield repo, temp_dir
82
+ finally:
83
+ repo.close()
84
+ else:
85
+ logging.info("get_repo_context: Using local repo...")
86
+ repo = Repo(".")
87
+ try:
88
+ yield repo, "."
89
+ finally:
90
+ repo.close()
gito/commands/deploy.py CHANGED
@@ -5,7 +5,7 @@ from microcore import ApiType, ui, utils
5
5
  from git import Repo
6
6
 
7
7
  from ..utils import version, extract_gh_owner_repo
8
- from ..bootstrap import app
8
+ from ..cli_base import app
9
9
 
10
10
 
11
11
  @app.command(name="deploy", help="Deploy Gito workflows to GitHub Actions")
@@ -56,11 +56,11 @@ def deploy(api_type: ApiType = None, commit: bool = None, rewrite: bool = False)
56
56
  remove_indent=True,
57
57
  )
58
58
  gito_code_review_yml = mc.tpl(
59
- "github_workflows/gito-code-review.yml.jinja2",
59
+ "github_workflows/gito-code-review.yml.j2",
60
60
  **template_vars
61
61
  )
62
62
  gito_react_to_comments_yml = mc.tpl(
63
- "github_workflows/gito-react-to-comments.yml.jinja2",
63
+ "github_workflows/gito-react-to-comments.yml.j2",
64
64
  **template_vars
65
65
  )
66
66
 
gito/commands/fix.py CHANGED
@@ -10,7 +10,7 @@ import git
10
10
  import typer
11
11
  from microcore import ui
12
12
 
13
- from ..bootstrap import app
13
+ from ..cli_base import app
14
14
  from ..constants import JSON_REPORT_FILE_NAME
15
15
  from ..report_struct import Report, Issue
16
16
 
@@ -5,7 +5,7 @@ from time import sleep
5
5
  import typer
6
6
  from ghapi.core import GhApi
7
7
 
8
- from ..bootstrap import app
8
+ from ..cli_base import app
9
9
  from ..constants import GITHUB_MD_REPORT_FILE_NAME, HTML_CR_COMMENT_MARKER
10
10
  from ..gh_api import (
11
11
  post_gh_comment,
@@ -15,8 +15,11 @@ from ..gh_api import (
15
15
  from ..project_config import ProjectConfig
16
16
 
17
17
 
18
- @app.command(help="Leave a GitHub PR comment with the review.")
19
- def github_comment(
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"),
20
23
  token: str = typer.Option(
21
24
  "", help="GitHub token (or set GITHUB_TOKEN env var)"
22
25
  ),
@@ -24,7 +27,7 @@ def github_comment(
24
27
  """
25
28
  Leaves a comment with the review on the current GitHub pull request.
26
29
  """
27
- file = GITHUB_MD_REPORT_FILE_NAME
30
+ file = md_report_file or GITHUB_MD_REPORT_FILE_NAME
28
31
  if not os.path.exists(file):
29
32
  logging.error(f"Review file not found: {file}, comment will not be posted.")
30
33
  raise typer.Exit(4)
@@ -38,25 +41,25 @@ def github_comment(
38
41
  raise typer.Exit(1)
39
42
  config = ProjectConfig.load()
40
43
  gh_env = config.prompt_vars["github_env"]
41
- gh_repo = gh_env.get("github_repo", "")
44
+ gh_repo = gh_repo or gh_env.get("github_repo", "")
42
45
  pr_env_val = gh_env.get("github_pr_number", "")
43
46
  logging.info(f"github_pr_number = {pr_env_val}")
44
47
 
45
- pr = None
46
- # e.g. could be "refs/pull/123/merge" or a direct number
47
- if "/" in pr_env_val and "pull" in pr_env_val:
48
- # refs/pull/123/merge
49
- try:
50
- pr_num_candidate = pr_env_val.strip("/").split("/")
51
- idx = pr_num_candidate.index("pull")
52
- pr = int(pr_num_candidate[idx + 1])
53
- except Exception:
54
- pass
55
- else:
56
- try:
57
- pr = int(pr_env_val)
58
- except ValueError:
59
- 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
60
63
  if not pr:
61
64
  if pr_str := os.getenv("PR_NUMBER_FROM_WORKFLOW_DISPATCH"):
62
65
  try:
@@ -16,7 +16,7 @@ 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
22
  from ..gh_api import post_gh_comment, resolve_gh_token
@@ -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,7 +16,7 @@ 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 *
gito/config.toml CHANGED
@@ -50,7 +50,7 @@ report_template_cli = """
50
50
  {%- if report.summary -%}
51
51
  {{- "\n" }}
52
52
  {{- "\n" }}{{- Style.BRIGHT }}✨ SUMMARY {{ Style.RESET_ALL -}}
53
- {{- "\n" }}{{- report.summary -}}
53
+ {{- "\n" }}{{- remove_html_comments(report.summary) -}}
54
54
  {%- endif %}
55
55
  {% for issue in report.plain_issues -%}
56
56
  {{"\n"}}{{ Style.BRIGHT }}{{Back.RED}}[ {{ issue.id}} ]{{Back.RESET}} {{ issue.title -}}{{ Style.RESET_ALL -}}
@@ -205,14 +205,16 @@ Summarize the code review in one sentence.
205
205
  {{ issues | tojson(indent=2) }}
206
206
  ---
207
207
  If the code changes include exceptional achievements, you may also present an award to the author in the summary text.
208
- 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.
209
210
  --Available Awards--
210
211
  {{ awards }}
211
212
  ---
212
213
  {% if pipeline_out.associated_issue and pipeline_out.associated_issue.title %}
213
214
  ----SUBTASK----
214
215
  Include one sentence about how the code changes address the requirements of the associated issue listed below.
215
-
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.
216
218
  --Associated Issue--
217
219
  # {{ pipeline_out.associated_issue.title }}
218
220
  {{ pipeline_out.associated_issue.description }}
@@ -223,10 +225,12 @@ Examples:
223
225
 
224
226
  If the implementation fully delivers the requested functionality:
225
227
  ```
228
+ <!-- issue_alignment -->
226
229
  ✅ Implementation Satisfies [<ISSUE_KEY>](<ISSUE_URL>).
227
230
  ```
228
231
  If there are concerns about how thoroughly the code covers the requirements and technical description from the associated issue:
229
232
  ```
233
+ <!-- issue_alignment -->
230
234
  ⚠️ <Describe specific gap or concern>.
231
235
  ⚠️ <Describe additional limitation or missing feature>.
232
236
  ```
@@ -253,6 +257,14 @@ Answer the following user question:
253
257
  --FILE: {{ file }}--
254
258
  {{ file_lines }}
255
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
+
256
268
  ----ANSWERING INSTRUCTIONS----
257
269
  {{ answering_instructions }}
258
270
  """
@@ -438,7 +450,9 @@ decorators add depth and texture, and observer masterfully completes the composi
438
450
  The Gang of Four gives a standing ovation from the stalls."
439
451
  ```
440
452
  """
441
- 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
+ """
442
456
  summary_requirements = ""
443
457
  answering_instructions = """
444
458
  - (!) Provide a concise, direct answer in engaging speech.
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,6 +3,7 @@ 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
9
  from microcore import ui
@@ -11,11 +12,13 @@ from git.exc import GitCommandError
11
12
  from unidiff import PatchSet, PatchedFile
12
13
  from unidiff.constants import DEV_NULL
13
14
 
15
+ from .context import Context
14
16
  from .project_config import ProjectConfig
15
17
  from .report_struct import Report
16
18
  from .constants import JSON_REPORT_FILE_NAME
17
- from .utils import stream_to_cli
19
+ from .utils import make_streaming_function
18
20
  from .pipeline import Pipeline
21
+ from .env import Env
19
22
 
20
23
 
21
24
  def review_subject_is_index(what):
@@ -88,7 +91,10 @@ def get_diff(
88
91
  else:
89
92
  current_ref = what
90
93
  merge_base = repo.merge_base(current_ref or repo.active_branch.name, against)[0]
91
-
94
+ logging.info(
95
+ f"Merge base({ui.green(current_ref)},{ui.yellow(against)})"
96
+ f" --> {ui.cyan(merge_base.hexsha)}"
97
+ )
92
98
  # if branch is already an ancestor of "against", merge_base == branch ⇒ it’s been merged
93
99
  if merge_base.hexsha == repo.commit(current_ref or repo.active_branch.name).hexsha:
94
100
  # @todo: check case: reviewing working copy index in main branch #103
@@ -104,19 +110,35 @@ def get_diff(
104
110
  '--pretty=format:%H'
105
111
  ).strip()
106
112
  if merge_sha:
113
+ logging.info(f"Merge commit is {ui.cyan(merge_sha)}")
107
114
  merge_commit = repo.commit(merge_sha)
108
115
 
109
116
  other_merge_parent = None
110
117
  for parent in merge_commit.parents:
118
+ logging.info(f"Checking merge parent: {parent.hexsha[:8]}")
111
119
  if parent.hexsha == merge_base.hexsha:
120
+ logging.info(f"merge parent is {ui.cyan(parent.hexsha[:8])}, skipping")
112
121
  continue
113
122
  if not commit_in_branch(repo, parent, against):
114
123
  logging.warning(f"merge parent is not in {against}, skipping")
115
124
  continue
125
+ logging.info(f"Found other merge parent: {ui.cyan(parent.hexsha[:8])}")
116
126
  other_merge_parent = parent
117
127
  break
118
128
  if other_merge_parent:
119
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
+ )
120
142
  logging.info(
121
143
  f"{what} will be compared to "
122
144
  f"first common ancestor of {what} and {against}: "
@@ -201,16 +223,17 @@ def file_lines(repo: Repo, file: str, max_tokens: int = None, use_local_files: b
201
223
  return "".join(lines)
202
224
 
203
225
 
204
- def make_cr_summary(config: ProjectConfig, report: Report, diff, **kwargs) -> str:
226
+ def make_cr_summary(ctx: Context, **kwargs) -> str:
205
227
  return (
206
228
  mc.prompt(
207
- config.summary_prompt,
208
- diff=mc.tokenizing.fit_to_token_size(diff, config.max_code_tokens)[0],
209
- issues=report.issues,
210
- **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,
211
234
  **kwargs,
212
235
  ).to_llm()
213
- if config.summary_prompt
236
+ if ctx.config.summary_prompt
214
237
  else ""
215
238
  )
216
239
 
@@ -322,12 +345,11 @@ async def review(
322
345
  out_folder = Path(out_folder or repo.working_tree_dir)
323
346
  out_folder.mkdir(parents=True, exist_ok=True)
324
347
  report = Report(issues=issues, number_of_processed_files=len(diff))
325
- ctx = dict(
348
+ ctx = Context(
326
349
  report=report,
327
350
  config=cfg,
328
351
  diff=diff,
329
352
  repo=repo,
330
- pipeline_out={},
331
353
  )
332
354
  if cfg.pipeline_steps:
333
355
  pipe = Pipeline(
@@ -338,7 +360,7 @@ async def review(
338
360
  else:
339
361
  logging.info("No pipeline steps defined, skipping pipeline execution")
340
362
 
341
- report.summary = make_cr_summary(**ctx)
363
+ report.summary = make_cr_summary(ctx)
342
364
  report.save(file_name=out_folder / JSON_REPORT_FILE_NAME)
343
365
  report_text = report.render(cfg, Report.Format.MARKDOWN)
344
366
  text_report_path = out_folder / "code-review-report.md"
@@ -353,20 +375,41 @@ def answer(
353
375
  against: str = None,
354
376
  filters: str | list[str] = "",
355
377
  use_merge_base: bool = True,
378
+ use_pipeline: bool = True,
379
+ prompt_file: str = None,
356
380
  ) -> str | None:
357
381
  try:
358
- repo, cfg, diff, lines = _prepare(
382
+ repo, config, diff, lines = _prepare(
359
383
  repo=repo, what=what, against=against, filters=filters, use_merge_base=use_merge_base
360
384
  )
361
385
  except NoChangesInContextError:
362
386
  logging.error("No changes to review")
363
387
  return
364
- response = mc.llm(mc.prompt(
365
- cfg.answer_prompt,
366
- question=question,
388
+
389
+ ctx = Context(
390
+ repo=repo,
367
391
  diff=diff,
368
- all_file_lines=lines,
369
- **cfg.prompt_vars,
370
- callback=stream_to_cli
371
- ))
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
+ )
372
415
  return response
gito/env.py ADDED
@@ -0,0 +1,3 @@
1
+ class Env:
2
+ logging_level: int = 1
3
+ verbosity: int = 1
gito/gh_api.py CHANGED
@@ -5,7 +5,7 @@ import requests
5
5
  from fastcore.basics import AttrDict # objects returned by ghapi
6
6
 
7
7
 
8
- def resolve_gh_token(token_or_none):
8
+ def resolve_gh_token(token_or_none: str | None = None) -> str | None:
9
9
  return token_or_none or os.getenv("GITHUB_TOKEN", None) or os.getenv("GH_TOKEN", None)
10
10
 
11
11
 
gito/pipeline.py CHANGED
@@ -2,10 +2,12 @@ import logging
2
2
  from enum import StrEnum
3
3
  from dataclasses import dataclass, field
4
4
 
5
- from gito.utils import is_running_in_github_action
6
5
  from microcore import ui
7
6
  from microcore.utils import resolve_callable
8
7
 
8
+ from .context import Context
9
+ from .utils import is_running_in_github_action
10
+
9
11
 
10
12
  class PipelineEnv(StrEnum):
11
13
  LOCAL = "local"
@@ -42,7 +44,7 @@ class PipelineStep:
42
44
 
43
45
  @dataclass
44
46
  class Pipeline:
45
- ctx: dict = field(default_factory=dict)
47
+ ctx: Context = field()
46
48
  steps: dict[str, PipelineStep] = field(default_factory=dict)
47
49
  verbose: bool = False
48
50
 
@@ -55,15 +57,14 @@ class Pipeline:
55
57
  def run(self, *args, **kwargs):
56
58
  cur_env = PipelineEnv.current()
57
59
  logging.info("Running pipeline... [env: %s]", ui.yellow(cur_env))
58
- self.ctx["pipeline_out"] = self.ctx.get("pipeline_out", {})
59
60
  for step_name, step in self.enabled_steps.items():
60
61
  if cur_env in step.envs:
61
62
  logging.info(f"Running pipeline step: {step_name}")
62
63
  try:
63
- step_output = step.run(*args, **kwargs, **self.ctx)
64
+ step_output = step.run(*args, **kwargs, **vars(self.ctx))
64
65
  if isinstance(step_output, dict):
65
- self.ctx["pipeline_out"].update(step_output)
66
- self.ctx["pipeline_out"][step_name] = step_output
66
+ self.ctx.pipeline_out.update(step_output)
67
+ self.ctx.pipeline_out[step_name] = step_output
67
68
  if self.verbose and step_output:
68
69
  logging.info(
69
70
  f"Pipeline step {step_name} output: {repr(step_output)}"
@@ -79,4 +80,4 @@ class Pipeline:
79
80
  f"Skipping pipeline step: {step_name}"
80
81
  f" [env: {ui.yellow(cur_env)} not in {step.envs}]"
81
82
  )
82
- return self.ctx["pipeline_out"]
83
+ return self.ctx.pipeline_out
@@ -2,7 +2,7 @@ import logging
2
2
  import os
3
3
 
4
4
  import git
5
- from jira import JIRA
5
+ from jira import JIRA, JIRAError
6
6
 
7
7
  from gito.issue_trackers import IssueTrackerIssue, resolve_issue_key
8
8
 
@@ -16,6 +16,11 @@ def fetch_issue(issue_key, jira_url, username, api_token) -> IssueTrackerIssue |
16
16
  description=issue.fields.description or "",
17
17
  url=f"{jira_url.rstrip('/')}/browse/{issue_key}"
18
18
  )
19
+ except JIRAError as e:
20
+ logging.error(
21
+ f"Failed to fetch Jira issue {issue_key}: code {e.status_code} :: {e.text}"
22
+ )
23
+ return None
19
24
  except Exception as e:
20
25
  logging.error(f"Failed to fetch Jira issue {issue_key}: {e}")
21
26
  return None
gito/report_struct.py CHANGED
@@ -12,7 +12,7 @@ import textwrap
12
12
 
13
13
  from .constants import JSON_REPORT_FILE_NAME, HTML_TEXT_ICON, HTML_CR_COMMENT_MARKER
14
14
  from .project_config import ProjectConfig
15
- from .utils import syntax_hint, block_wrap_lr, max_line_len
15
+ from .utils import syntax_hint, block_wrap_lr, max_line_len, remove_html_comments
16
16
 
17
17
 
18
18
  @dataclass
@@ -126,6 +126,7 @@ class Report:
126
126
  max_line_len=max_line_len,
127
127
  HTML_TEXT_ICON=HTML_TEXT_ICON,
128
128
  HTML_CR_COMMENT_MARKER=HTML_CR_COMMENT_MARKER,
129
+ remove_html_comments=remove_html_comments,
129
130
  **config.prompt_vars
130
131
  )
131
132
 
@@ -1,6 +1,6 @@
1
1
 
2
2
  LLM_API_TYPE: {{ api_type }}
3
- LLM_API_KEY: {{ "${{ secrets." + secret_name + "}}" }}
3
+ LLM_API_KEY: {{ "${{ secrets." + secret_name + " }}" }}
4
4
  MODEL: {{ model }}
5
5
  {% raw -%}
6
6
  JIRA_TOKEN: ${{ secrets.JIRA_TOKEN }}
@@ -0,0 +1,23 @@
1
+
2
+
3
+ - name: Set up Python
4
+ uses: actions/setup-python@v5
5
+ with: { python-version: "3.13" }
6
+
7
+ - name: Fetch Latest Gito Version
8
+ id: gito-version
9
+ run: pip index versions gito.bot 2>/dev/null | head -1 | sed -n 's/.* (\([^)]*\)).*/version=\1/p' >> $GITHUB_OUTPUT
10
+
11
+ {% raw -%}
12
+ - uses: actions/cache@v4
13
+ id: cache
14
+ with:
15
+ path: |
16
+ ${{ env.pythonLocation }}/lib/python3.13/site-packages
17
+ ${{ env.pythonLocation }}/bin
18
+ key: gito_v${{ steps.gito-version.outputs.version }}
19
+ {%- endraw %}
20
+
21
+ - name: Install Gito
22
+ if: steps.cache.outputs.cache-hit != 'true'
23
+ run: pip install gito.bot~={{ major }}.{{ minor }}
@@ -15,15 +15,15 @@ jobs:
15
15
  - uses: actions/checkout@v4
16
16
  with: { fetch-depth: 0 }
17
17
 
18
- {%- include("github_workflows/components/installs.jinja2") %}
18
+ {%- include("github_workflows/components/installs.j2") %}
19
19
 
20
20
  - name: Run AI code review
21
21
  env:
22
- {%- include("github_workflows/components/env-vars.jinja2") %}
22
+ {%- include("github_workflows/components/env-vars.j2") %}
23
23
  PR_NUMBER_FROM_WORKFLOW_DISPATCH: {% raw %}${{ github.event.inputs.pr_number }}{% endraw %}
24
- run: |
25
- gito --verbose review
26
- gito github-comment --token {% raw %}${{ secrets.GITHUB_TOKEN }}{% endraw %}
24
+ run: |{% raw %}
25
+ gito --verbose review ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref && format(' --against="origin/{0}"', github.event.pull_request.base.ref) || '' }}
26
+ gito github-comment --token ${{ secrets.GITHUB_TOKEN }}{% endraw %}
27
27
 
28
28
  - uses: actions/upload-artifact@v4
29
29
  with:
@@ -59,12 +59,12 @@ jobs:
59
59
  fetch-depth: 0
60
60
  {%- endraw %}
61
61
 
62
- {%- include("github_workflows/components/installs.jinja2") %}
62
+ {%- include("github_workflows/components/installs.j2") %}
63
63
 
64
64
  - name: Run Gito react
65
65
  env:
66
66
  # LLM config is needed only if answer_github_comments = true in .gito/config.toml
67
67
  # Otherwise, use LLM_API_TYPE: none
68
- {%- include("github_workflows/components/env-vars.jinja2") %}
68
+ {%- include("github_workflows/components/env-vars.j2") %}
69
69
  run: |
70
70
  {% raw %}gito react-to-comment ${{ github.event.comment.id }} --token ${{ secrets.GITHUB_TOKEN }}{%- endraw %}
@@ -0,0 +1,24 @@
1
+ {{ self_id }}
2
+ ----TASK----
3
+ Write release notes for public documentation.
4
+ Summarize the following changes, focusing on what is new, improved, or fixed for the end user.
5
+ Do not include internal or technical details.
6
+ Structure release notes using clear sections: Added, Changed, Fixed.
7
+ Avoid internal technical jargon or developer-specific details.
8
+
9
+
10
+ ----RELATED CODEBASE CHANGES----
11
+ {% for part in diff %}{{ part }}\n{% endfor %}
12
+
13
+ ----FULL FILE CONTENT AFTER APPLYING CHANGES----
14
+ {% for file, file_lines in all_file_lines.items() %}
15
+ --FILE: {{ file }}--
16
+ {{ file_lines }}
17
+ {% endfor %}
18
+
19
+ {%- if pipeline_out.associated_issue and pipeline_out.associated_issue.title %}
20
+ ----ASSOCIATED ISSUE----
21
+ # {{ pipeline_out.associated_issue.title }}
22
+ {{ pipeline_out.associated_issue.description }}
23
+ URL: {{ pipeline_out.associated_issue.url }}
24
+ {%- endif -%}{{ '\n' }}
gito/utils.py CHANGED
@@ -3,11 +3,11 @@ import sys
3
3
  import os
4
4
  from pathlib import Path
5
5
  import importlib.metadata
6
+ from typing import Optional
6
7
 
7
8
  import typer
8
9
  import git
9
10
  from git import Repo
10
- from microcore import ui
11
11
 
12
12
 
13
13
  _EXT_TO_HINT: dict[str, str] = {
@@ -223,9 +223,20 @@ def detect_github_env() -> dict:
223
223
  return d
224
224
 
225
225
 
226
- def stream_to_cli(text):
227
- print(ui.blue(text), end='')
226
+ def make_streaming_function(handler: Optional[callable] = None) -> callable:
227
+ def stream(text):
228
+ if handler:
229
+ text = handler(text)
230
+ print(text, end='', flush=True)
231
+ return stream
228
232
 
229
233
 
230
234
  def version() -> str:
231
235
  return importlib.metadata.version("gito.bot")
236
+
237
+
238
+ def remove_html_comments(text):
239
+ """
240
+ Removes all HTML comments (<!-- ... -->) from the input text.
241
+ """
242
+ return re.sub(r'<!--.*?-->\s*', '', text, flags=re.DOTALL)
@@ -1,31 +0,0 @@
1
- gito/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- gito/__main__.py,sha256=MSmt_5Xg84uHqzTN38JwgseJK8rsJn_11A8WD99VtEo,61
3
- gito/bootstrap.py,sha256=vGI19asH0-GFj3_w_wqWucSAWPwgCP4-FkTgVuvyIGY,2517
4
- gito/cli.py,sha256=_OwUeOQr3seMVp6ZIOfHHvKBJIKWb3-kQic5nZ2tmgE,6563
5
- gito/commands/__init__.py,sha256=B2uUQsLMEsHfNT1N3lWYm38WSuQIHFmjiGs2tdBuDBA,55
6
- gito/commands/deploy.py,sha256=1199AisP3RIpz5v48JTPmQEcA7KIWqYF5lLkU2HEiJ4,3758
7
- gito/commands/fix.py,sha256=OS829xVrLMLQqlol9AoxJMZ9qaAWRCSbUkVNszK96ws,5305
8
- gito/commands/gh_post_review_comment.py,sha256=BTLcS5-pfmGtVjpQEG77jexFVvzme94_DMU51icaCpQ,3517
9
- gito/commands/gh_react_to_comment.py,sha256=Pu7T-rBB-jTJg1oUO34_FfpJsVgEIw-7rZRkTAL72tc,6396
10
- gito/commands/repl.py,sha256=wiM5vbGNIgAeyjOKt9QhrRdGYB13Md-QYx41dMIPvaU,544
11
- gito/config.toml,sha256=2gYU8R6UvYZg3ZniN4s2-kECmbC9k_K0nrk38-I1bKo,17596
12
- gito/constants.py,sha256=_G40SAMfuYjYsBzEVUIxKT8Vo0dAWIQqCWWxdaBAltY,766
13
- gito/core.py,sha256=tpPTMtjBCFtXuVshdbmL3fO6gc-bG-zlIPmnXCRdOhc,12648
14
- gito/gh_api.py,sha256=FlqENZo0nmdtAozaKYj582w05gfs3PpEevS48hyup3M,2925
15
- gito/issue_trackers.py,sha256=XYspyaIuf0ANQSvDUea5_oOdo9tQvpZZsapI5S9g78U,1551
16
- gito/pipeline.py,sha256=Nq5VUVrXVDXVVYX6nVSMX1CyhoWsNtSeTOQyNgQAsdE,2672
17
- gito/pipeline_steps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
- gito/pipeline_steps/jira.py,sha256=cgRSHoU6j0lcK6hQfhg950PEmIu3uWLGMKZbW3-H3mg,1683
19
- gito/pipeline_steps/linear.py,sha256=6UDc8nGKGpwHruPq8VItE2QBWshWxaTapoMhu_qjN_g,2445
20
- gito/project_config.py,sha256=jOPVMgN01_krELlNXzZlrZOKmXZP-RLDS7iOgVSUyic,3182
21
- gito/report_struct.py,sha256=HuvetI4XWTNEm-afS9Fx2doTP0ybn0P22UbH-55ambs,4243
22
- gito/tpl/github_workflows/components/env-vars.jinja2,sha256=GPlIyhspqJFQLLjTAKz8yKeU563bsUs_n-Ao1MGmh_Y,355
23
- gito/tpl/github_workflows/components/installs.jinja2,sha256=fQJAici1KiF3C35leB8ovO7zAvwBvcZy9tSCHKyIdkI,186
24
- gito/tpl/github_workflows/gito-code-review.yml.jinja2,sha256=KpZdOsjINPT0TuL8FYA9TxK84hRqbGIkFjOI1XNew2g,1007
25
- gito/tpl/github_workflows/gito-react-to-comments.yml.jinja2,sha256=yPKa285eQZBHSbmSknmcQjfVqBzgUHR3SvBJg7u1Jsc,2173
26
- gito/utils.py,sha256=SOD89s5qpr0fToumUl7hYAPsOKfix_8MjoxeUks5s8I,6518
27
- ai_cr-2.0.3.dist-info/LICENSE,sha256=VbdF_GbbDK24JvdTfnsxa2M6jmhsxmRSFeHCx-lICGE,1075
28
- ai_cr-2.0.3.dist-info/METADATA,sha256=f6L-_hbYBYIdoYdigM_pdw4oUuzrgyMj_IGslT5-A-o,7989
29
- ai_cr-2.0.3.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
30
- ai_cr-2.0.3.dist-info/entry_points.txt,sha256=Ua1DxkhJJ8TZuLgnH-IlWCkrre_0S0dq_GtYRaYupWk,38
31
- ai_cr-2.0.3.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
-
2
-
3
- - name: Set up Python
4
- uses: actions/setup-python@v5
5
- with: { python-version: "3.13" }
6
-
7
- - name: Install Gito
8
- run: pip install gito.bot~={{ major }}.{{ minor }}
File without changes
File without changes