gitpr-cli 1.7.0__py3-none-any.whl → 1.8.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.4
2
2
  Name: gitpr-cli
3
- Version: 1.7.0
3
+ Version: 1.8.0
4
4
  Summary: AI-powered PR automation, commit messages, and code review (Gemini, DeepSeek, and Ollama)
5
5
  Author-email: Natan Fiuza <contato@natanfiuza.dev.br>
6
6
  Requires-Python: >=3.10
@@ -499,7 +499,7 @@ This is useful for scripting, CI/CD pipelines, and one-off queries where you don
499
499
 
500
500
  📖 **Full documentation:** [gitpr.natanfiuza.dev.br/docs/mcp-integration](https://gitpr.natanfiuza.dev.br/docs/mcp-integration) — available in 5 languages (EN, PT-BR, PT-PT, ES, FR).
501
501
 
502
- > 🧩 **Official VS Code & Cursor Extension** — Prefer native editor commands, SCM toolbar buttons, and inline `Problems` diagnostics? Download the ready-to-use `.vsix` from [GitHub Releases](https://github.com/natanfiuza/gitpr/releases) or see the [Editor Extension Guide](https://gitpr.natanfiuza.dev.br/docs/editor-extension) for 1-click install in VS Code and Cursor.
502
+ > 🧩 **Official VS Code & Cursor Extension** — Prefer native editor commands, SCM toolbar buttons, and inline `Problems` diagnostics? Download the ready-to-use `.vsix` from [GitHub Releases](https://github.com/natanfiuza/gitpr/releases) or see the [Editor Extension Guide](https://gitpr.natanfiuza.dev.br/docs/editor-extension) for 1-click install in VS Code and Cursor. Maintainers can run `scripts/marketplace-setup-wizard.sh` to configure and publish to Visual Studio Marketplace and Open VSX Registry.
503
503
 
504
504
  > 💬 **MCP Prompts** — GitPR also exposes 7 pre-defined message templates (prompts) for common flows like "Review PR", "Generate Commit Message", and "Create Issue from Diff". See the [MCP Prompts guide](https://gitpr.natanfiuza.dev.br/docs/mcp-prompts) for the full list.
505
505
 
@@ -1,4 +1,4 @@
1
- gitpr_cli-1.7.0.dist-info/licenses/LICENSE,sha256=UQzLC6mwqb-ZNDkBBAqOxC-SMQcEID0gaM6oFcu6u48,27030
1
+ gitpr_cli-1.8.0.dist-info/licenses/LICENSE,sha256=UQzLC6mwqb-ZNDkBBAqOxC-SMQcEID0gaM6oFcu6u48,27030
2
2
  src/__init__.py,sha256=ZHymXsocMDazI_fjOIgAQsRQvFPN2tV8FOuv0qptI_o,30
3
3
  src/ai_providers.py,sha256=QyFEOsXyA59lZziO5bl-4nlYFJpxBcxU9aLhcTBnkWE,14633
4
4
  src/ascii_chart.py,sha256=WFQclaxm5W6bPBBNVlImyrnrJYCSj5gRgEpldYzI7wk,8279
@@ -7,14 +7,14 @@ src/cache.py,sha256=frROc69NyTn3TdJIOPctvuH9z_sh2pdKxL4ekMtyh6k,6572
7
7
  src/changelog_builder.py,sha256=Ea0coS6LCqsdxTWrKW8SZyVsD02HuNNYzCoggI0loi8,9922
8
8
  src/chat_memory.py,sha256=PcohnyspAfIwwwbsp9U2Wk_1AylJMFEMoWqRSJe_w30,8949
9
9
  src/commit_classifier.py,sha256=HQHxPbCfgW_GD1_4mbO2No16jh_Ci5RTSoLbTU4IWYI,6920
10
- src/config.py,sha256=MrfZW27x-41gI9P8gtAyMuTryzeluBtvZfxRZkVIiBk,47122
11
- src/config_schema.py,sha256=VATmOHWs_eeMLVhrtGFANcOVdrYwSadiFPUUhZM33OE,58091
12
- src/core.py,sha256=RmwqnEa0-lEHKpxCIH95JLJkJQGsfx_QC8n8H_uJitQ,99037
10
+ src/config.py,sha256=1U9dZyfk_OsMzoIcYIbvh_N1H9ZO34PikbjjCHzg7PI,48895
11
+ src/config_schema.py,sha256=qClFT_qoa_oV3yyPENiT79MB4AFsuXvsSlo4an-DIcE,60375
12
+ src/core.py,sha256=9IuJPJecTyATUzI4qCXWv67PnfzQMulrKOOb24CdN3U,106050
13
13
  src/diff_parser.py,sha256=JHeaTJflNOEbR9SYK_Ljr2hctrrABUIb31IFHAbyIDo,15405
14
14
  src/doc_links.py,sha256=GdANhE6XbEnVDsbpCf22eUZnqFXor_2SU1O4S-pteIE,2028
15
15
  src/github_api.py,sha256=i1o58oKWWmMCerP-g-hOQHGCwPWd01ooKntTKjpFhIA,3948
16
16
  src/i18n.py,sha256=Xp9c1dNGmTHbo8SyXa-LQuO71nWx3RP-Us7jJ7ARtUY,4804
17
- src/issue_engine.py,sha256=RmZsQVg6N1vCcpMdv_5FX-cMGgQ3I0vwMN4parQ4zFg,10974
17
+ src/issue_engine.py,sha256=fgf1SO0ENJmHTKP4NFlHaT2jdOWgWAVdU0yRoQq2-hc,11171
18
18
  src/ledger.py,sha256=3p5nX9gPGHgANQxBxEywrKo3TNb5KdWDeTI2mslYopo,43787
19
19
  src/linter_engine.py,sha256=hIxht8M09Fnm6NiJZOqjI6dyqnFoHUDVA8Ve4dWZV4s,25214
20
20
  src/linter_wizard.py,sha256=mo8aSTZmVLluCOskWv3nji74WPJh_6c122zwADN9aCc,7634
@@ -26,14 +26,14 @@ src/project_config.py,sha256=r-gzjKeFuTYvUzOflQlpbijn6uLXtPEUBzA-1PzUR_k,16749
26
26
  src/release_engine.py,sha256=S0kgO-a7RSC07AzvRWRlGo32jx38PAQGIzgvYXOfthk,41274
27
27
  src/reviewer_resolution.py,sha256=bFRdf4GVWIpz4kk7cyF7EYWV43aP7V8sAlNGqKRPd7A,5913
28
28
  src/reviewer_suggestion.py,sha256=YeXJLjJjn1YS69FChWZBF4qHUuIwvv_n-GPiymLN8LQ,8904
29
- src/scm_settings.py,sha256=3pXgBbxqekvD4LM-I3GgfV4IOa6QJn4x3-DPAThCris,14156
29
+ src/scm_settings.py,sha256=ZNMgr893qj-B3DjbXkuINGnJYvJU_WvHvi535ZjdVow,14698
30
30
  src/security.py,sha256=fkWy-r4tdISH0xOyWZyR8ZCFyLuYqu8fn9sKsIeNAso,1351
31
31
  src/security_ruleset.py,sha256=4mqmqtWLHuhuCcjjaoKUM_YFzil4cWZ_ubjlOLgbM_4,4874
32
32
  src/skill_frontmatter.py,sha256=K-HsjwSYPHUsDRjIiugBE3koxVdGf38L5d4l8eYVqEo,2167
33
33
  src/spinner.py,sha256=JGOO_taTXbrQK0KnNxUQ40PcpS802bYYBosWWm_NG9o,9012
34
34
  src/suggest_reviewers.py,sha256=LTOtwDIjaBS_FGDU6bjN6oGy-YD112hIPugYytNKD_A,5821
35
35
  src/tui_issue.py,sha256=yaNpj9SSXiWWjMJ7a9FABcNWSffCkA1tjUSnJ1hN-MQ,11477
36
- src/updater.py,sha256=8OwxL8hhh-hzKyQo5sjckw_e4bNlmlKeegwwH35nrG8,9560
36
+ src/updater.py,sha256=ECdRPKQ-_sKDi4LFn2MlMXOjTPFx7QdJApjmv5pY_lE,9560
37
37
  src/usage_log.py,sha256=CBCNlUm_xftXsBaGRHvsEjTENvZfE-lW70HGLMkisSg,4508
38
38
  src/version_bump.py,sha256=Dw1LZ_c6VzaO_s9cDeYSevoWtXVr_oP70eS_mkrOm8s,3041
39
39
  src/application/__init__.py,sha256=3YXFcmxJh0f7BwryKXxUrOhGrLnZLsagV-VbBIk0eGk,31
@@ -128,7 +128,7 @@ src/infrastructure/scm/__init__.py,sha256=CbW2p9DsYpwwUqy4xdtgoPugsLijopfNwLT-IA
128
128
  src/infrastructure/scm/azure_devops_provider.py,sha256=9IpgB-Vqzgj0VDFXaYs4PFRiecsQGU9svW5m3OpefsI,18790
129
129
  src/infrastructure/scm/base.py,sha256=9tN2IivEngHoo1pDK3PivWHvHSkXokuyQQUJ680gsXU,17198
130
130
  src/infrastructure/scm/bitbucket_provider.py,sha256=A58dMDa60lLUynENrinWENFz5dSA_lb9VJSg22ptbkI,16895
131
- src/infrastructure/scm/factory.py,sha256=O3bRT6f15AozcldZfiH2LgOQ4g4GqMb0sbEWCIeF0HE,4487
131
+ src/infrastructure/scm/factory.py,sha256=olcDDs0tsa8Ae0vKhhW5DnS408xRhn5fxv_0DskFCgY,4665
132
132
  src/infrastructure/scm/github_provider.py,sha256=vdJDTTYd-CYkAQos-6IZ2hFOJkXG09SrkIcZzHbuuIc,21316
133
133
  src/infrastructure/scm/gitlab_provider.py,sha256=VAYjdg5gU43FDamXUNMdFzs9MwIGyNRqMzEnLTacRE8,17951
134
134
  src/infrastructure/scm/web_links.py,sha256=-7oF7oUwK_UvKVJZWDPbTnKBi9JEQu2dMDYtgqH10iM,3724
@@ -153,7 +153,7 @@ src/policy_packs/vue-quality/policy.yml,sha256=FyVL17Fl_bgVjxZU5lO7MME5w04vjKV3U
153
153
  src/review/__init__.py,sha256=ux2rkwebfnzuQpFc57eEceOC4CZkojPOtfp951Ux5Gc,700
154
154
  src/review/diff_normalizer.py,sha256=8KnFbauGK-fuLGSSL_ox4kAwCE048MuTmorPwBrhlEU,3675
155
155
  src/review/diff_source.py,sha256=V6fMtgxNJbMBL_qRifkLGfzFwtaTCbFFKFvcutHZjLk,2733
156
- src/review/remote_pr.py,sha256=9oEEo-v-D08egxwN_EINh5Ju74vow91-CAH1kAb2ql0,13669
156
+ src/review/remote_pr.py,sha256=f9gkvK4uw--Np_la4lRuSkhmEplUccI5ePCllJFj-Lo,13884
157
157
  src/review/render.py,sha256=l63vQ3WiGxPSzCk4JxyQoFNfIyG0AdHgMobAjMeaOcY,3194
158
158
  src/split/__init__.py,sha256=8XZn4jwPhQ0pJyNEMTBfgbGTMJkQUvePNp5ZncWCJ-Q,929
159
159
  src/split/apply_split_plan.py,sha256=ZKoACWLCaBndLzvRl8wHyAyPlalTrjrPJ2ep_AjpWCE,10929
@@ -177,8 +177,8 @@ src/ui/demo/__init__.py,sha256=aOdUN0izuwW2FfLAHG9Qga-DJG5NBVqP0gYNO8OiKls,59
177
177
  src/ui/demo/demo_app.py,sha256=4t_Mct-BJXpe5tlt1XmtRyN1IT0OL6U3WSmGqOJqMG4,3165
178
178
  src/ui/demo/demo_help_screen.py,sha256=I_LELswhPtbHjfMWGVUqLtOwxGbzSYJsVWXjqZrLoLA,2108
179
179
  src/ui/demo/demo_screens.py,sha256=GXaQFTgTn9awSP-EYiBXsCZqB583WrSQcuNYoBK12g8,1997
180
- gitpr_cli-1.7.0.dist-info/METADATA,sha256=hpoCCrRp4v_yMf_v7Pqsg4YtA0nVWFha4N2JugWwr7E,54113
181
- gitpr_cli-1.7.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
182
- gitpr_cli-1.7.0.dist-info/entry_points.txt,sha256=TstGnMUzdOI5qNmpM97EPDBPuMqOZxoo6EoLxHSOMSQ,71
183
- gitpr_cli-1.7.0.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
184
- gitpr_cli-1.7.0.dist-info/RECORD,,
180
+ gitpr_cli-1.8.0.dist-info/METADATA,sha256=6tgDZ4ZC09Mkw_S35xuA2v_ciLAMqFoM7b5buvBxdNM,54248
181
+ gitpr_cli-1.8.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
182
+ gitpr_cli-1.8.0.dist-info/entry_points.txt,sha256=TstGnMUzdOI5qNmpM97EPDBPuMqOZxoo6EoLxHSOMSQ,71
183
+ gitpr_cli-1.8.0.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
184
+ gitpr_cli-1.8.0.dist-info/RECORD,,
src/config.py CHANGED
@@ -249,6 +249,59 @@ def get_linter_timeout():
249
249
  return _positive_float(os.getenv("GITPR_LINTER_TIMEOUT"), _DEFAULT_LINTER_TIMEOUT)
250
250
 
251
251
 
252
+ DEFAULT_MAP_REDUCE_MAX_TOKENS = 90000
253
+ DEFAULT_MAP_REDUCE_MAX_PART_TOKENS = 50000
254
+
255
+
256
+ def _positive_int_from_raw(raw, fallback):
257
+ """Parses raw as a positive int, falling back on non-positive or invalid input."""
258
+ if raw is None:
259
+ return fallback
260
+ try:
261
+ val = int(str(raw).strip())
262
+ return val if val > 0 else fallback
263
+ except (TypeError, ValueError):
264
+ return fallback
265
+
266
+
267
+ def get_map_reduce_thresholds(provider=None):
268
+ """Returns (max_tokens, max_part_tokens) for Map-Reduce chunking.
269
+
270
+ Resolution hierarchy:
271
+ 1. Provider-specific: GITPR_MAP_REDUCE_MAX_TOKENS_<PROVIDER>
272
+ 2. Generic: GITPR_MAP_REDUCE_MAX_TOKENS
273
+ 3. Baseline default: 90,000 tokens
274
+
275
+ For part tokens:
276
+ 1. Provider-specific: GITPR_MAP_REDUCE_MAX_PART_TOKENS_<PROVIDER>
277
+ 2. Generic: GITPR_MAP_REDUCE_MAX_PART_TOKENS
278
+ 3. Baseline default: 50,000 tokens
279
+
280
+ Guarantees max_part_tokens <= max_tokens.
281
+ """
282
+ load_dotenv(ENV_FILE)
283
+ prov_key = (provider or "").strip().upper()
284
+
285
+ trigger_val = None
286
+ if prov_key:
287
+ trigger_val = os.getenv(f"GITPR_MAP_REDUCE_MAX_TOKENS_{prov_key}")
288
+ if trigger_val is None or not str(trigger_val).strip():
289
+ trigger_val = os.getenv("GITPR_MAP_REDUCE_MAX_TOKENS")
290
+ max_tokens = _positive_int_from_raw(trigger_val, DEFAULT_MAP_REDUCE_MAX_TOKENS)
291
+
292
+ part_val = None
293
+ if prov_key:
294
+ part_val = os.getenv(f"GITPR_MAP_REDUCE_MAX_PART_TOKENS_{prov_key}")
295
+ if part_val is None or not str(part_val).strip():
296
+ part_val = os.getenv("GITPR_MAP_REDUCE_MAX_PART_TOKENS")
297
+ max_part_tokens = _positive_int_from_raw(part_val, DEFAULT_MAP_REDUCE_MAX_PART_TOKENS)
298
+
299
+ if max_part_tokens > max_tokens:
300
+ max_part_tokens = max_tokens
301
+
302
+ return max_tokens, max_part_tokens
303
+
304
+
252
305
  # Metrics pricing.
253
306
  #
254
307
  # Two layers, the second overriding the first per model:
src/config_schema.py CHANGED
@@ -483,6 +483,28 @@ FIELDS = (
483
483
  validator=VALIDATOR_AI_KEY,
484
484
  show_if=(("DEFAULT_AI_PROVIDER", ("gemini",)),),
485
485
  ),
486
+ ConfigField(
487
+ key="GITPR_MAP_REDUCE_MAX_TOKENS_GEMINI",
488
+ label=__("Gemini Map-Reduce Trigger Tokens"),
489
+ description=__(
490
+ "Maximum total diff tokens before triggering Map-Reduce processing for Gemini."
491
+ ),
492
+ category="ai",
493
+ kind=KIND_INT,
494
+ default="90000",
495
+ show_if=(("DEFAULT_AI_PROVIDER", ("gemini",)),),
496
+ ),
497
+ ConfigField(
498
+ key="GITPR_MAP_REDUCE_MAX_PART_TOKENS_GEMINI",
499
+ label=__("Gemini Map-Reduce Part Size"),
500
+ description=__(
501
+ "Maximum tokens per batch chunk during Gemini Map-Reduce processing."
502
+ ),
503
+ category="ai",
504
+ kind=KIND_INT,
505
+ default="50000",
506
+ show_if=(("DEFAULT_AI_PROVIDER", ("gemini",)),),
507
+ ),
486
508
  ConfigField(
487
509
  key="DEEPSEEK_API_MODEL_PRIMARY",
488
510
  label=__("DeepSeek Advanced Model"),
@@ -559,6 +581,28 @@ FIELDS = (
559
581
  validator=VALIDATOR_AI_KEY,
560
582
  show_if=(("DEFAULT_AI_PROVIDER", ("deepseek",)),),
561
583
  ),
584
+ ConfigField(
585
+ key="GITPR_MAP_REDUCE_MAX_TOKENS_DEEPSEEK",
586
+ label=__("DeepSeek Map-Reduce Trigger Tokens"),
587
+ description=__(
588
+ "Maximum total diff tokens before triggering Map-Reduce processing for DeepSeek."
589
+ ),
590
+ category="ai",
591
+ kind=KIND_INT,
592
+ default="90000",
593
+ show_if=(("DEFAULT_AI_PROVIDER", ("deepseek",)),),
594
+ ),
595
+ ConfigField(
596
+ key="GITPR_MAP_REDUCE_MAX_PART_TOKENS_DEEPSEEK",
597
+ label=__("DeepSeek Map-Reduce Part Size"),
598
+ description=__(
599
+ "Maximum tokens per batch chunk during DeepSeek Map-Reduce processing."
600
+ ),
601
+ category="ai",
602
+ kind=KIND_INT,
603
+ default="50000",
604
+ show_if=(("DEFAULT_AI_PROVIDER", ("deepseek",)),),
605
+ ),
562
606
  ConfigField(
563
607
  key="OLLAMA_API_MODEL_PRIMARY",
564
608
  label=__("Ollama Advanced Model"),
@@ -579,6 +623,28 @@ FIELDS = (
579
623
  show_if=(("DEFAULT_AI_PROVIDER", ("ollama",)),),
580
624
  seeded=True,
581
625
  ),
626
+ ConfigField(
627
+ key="GITPR_MAP_REDUCE_MAX_TOKENS_OLLAMA",
628
+ label=__("Ollama Map-Reduce Trigger Tokens"),
629
+ description=__(
630
+ "Maximum total diff tokens before triggering Map-Reduce processing for Ollama."
631
+ ),
632
+ category="ai",
633
+ kind=KIND_INT,
634
+ default="90000",
635
+ show_if=(("DEFAULT_AI_PROVIDER", ("ollama",)),),
636
+ ),
637
+ ConfigField(
638
+ key="GITPR_MAP_REDUCE_MAX_PART_TOKENS_OLLAMA",
639
+ label=__("Ollama Map-Reduce Part Size"),
640
+ description=__(
641
+ "Maximum tokens per batch chunk during Ollama Map-Reduce processing."
642
+ ),
643
+ category="ai",
644
+ kind=KIND_INT,
645
+ default="50000",
646
+ show_if=(("DEFAULT_AI_PROVIDER", ("ollama",)),),
647
+ ),
582
648
  # ----------------------------------------------------------- Pull request
583
649
  ConfigField(
584
650
  key="OUTPUT_FILE_NAME",
src/core.py CHANGED
@@ -1099,20 +1099,136 @@ def estimate_token_count(text):
1099
1099
  return len(text) // 4
1100
1100
 
1101
1101
 
1102
- def split_diff_into_chunks(diff_text, max_tokens=90000):
1103
- """Splits the diff based on the token limit while preserving file header integrity."""
1102
+ def _split_line_chunk(text, max_tokens):
1103
+ """Splits arbitrary text by lines (and character slices if needed) keeping each part <= max_tokens."""
1104
+ chunks = []
1105
+ current = ""
1106
+ for line in text.splitlines(keepends=True):
1107
+ if estimate_token_count(line) > max_tokens:
1108
+ if current:
1109
+ chunks.append(current)
1110
+ current = ""
1111
+ step = max_tokens * 4
1112
+ for start in range(0, len(line), step):
1113
+ slice_str = line[start : start + step]
1114
+ chunks.append(slice_str)
1115
+ elif estimate_token_count(current + line) > max_tokens and current:
1116
+ chunks.append(current)
1117
+ current = line
1118
+ else:
1119
+ current += line
1120
+ if current:
1121
+ chunks.append(current)
1122
+ return chunks or [text]
1123
+
1124
+
1125
+ def _split_giant_file(file_diff, max_part_tokens):
1126
+ """Splits a single file diff that exceeds max_part_tokens into smaller parts."""
1127
+ fn_match = re.search(r"^diff --git a/(\S+) b/\S+", file_diff, flags=re.MULTILINE)
1128
+ file_name = fn_match.group(1) if fn_match else "file"
1129
+
1130
+ hunk_match = re.search(
1131
+ r"^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@", file_diff, flags=re.MULTILINE
1132
+ )
1133
+ if hunk_match:
1134
+ header = file_diff[: hunk_match.start()]
1135
+ body = file_diff[hunk_match.start() :]
1136
+ else:
1137
+ header = ""
1138
+ body = file_diff
1139
+
1140
+ hunk_parts = re.split(
1141
+ r"(^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@.*)", body, flags=re.MULTILINE
1142
+ )
1143
+ raw_hunks = []
1144
+ if len(hunk_parts) > 1:
1145
+ if hunk_parts[0].strip():
1146
+ raw_hunks.append(hunk_parts[0])
1147
+ for j in range(1, len(hunk_parts), 2):
1148
+ hunk_content = hunk_parts[j] + (
1149
+ hunk_parts[j + 1] if j + 1 < len(hunk_parts) else ""
1150
+ )
1151
+ raw_hunks.append(hunk_content)
1152
+ else:
1153
+ raw_hunks = [body]
1154
+
1155
+ sub_chunks = []
1156
+ current_sub = ""
1157
+ header_clean = header.strip()
1158
+
1159
+ def _format_part(sub_content, idx):
1160
+ prefix = (
1161
+ f"{header_clean}\n# [{file_name} - part {idx}]\n\n"
1162
+ if header_clean
1163
+ else f"# [{file_name} - part {idx}]\n\n"
1164
+ )
1165
+ return prefix + sub_content
1166
+
1167
+ part_idx = 1
1168
+ for hunk in raw_hunks:
1169
+ if estimate_token_count(hunk) > max_part_tokens:
1170
+ if current_sub:
1171
+ sub_chunks.append(_format_part(current_sub, part_idx))
1172
+ part_idx += 1
1173
+ current_sub = ""
1174
+ line_pieces = _split_line_chunk(hunk, max_part_tokens)
1175
+ for piece in line_pieces:
1176
+ sub_chunks.append(_format_part(piece, part_idx))
1177
+ part_idx += 1
1178
+ elif (
1179
+ estimate_token_count(current_sub + hunk) > max_part_tokens
1180
+ and current_sub
1181
+ ):
1182
+ sub_chunks.append(_format_part(current_sub, part_idx))
1183
+ part_idx += 1
1184
+ current_sub = hunk
1185
+ else:
1186
+ current_sub += hunk
1187
+
1188
+ if current_sub:
1189
+ sub_chunks.append(_format_part(current_sub, part_idx))
1190
+
1191
+ return sub_chunks or [file_diff]
1192
+
1193
+
1194
+ def split_diff_into_chunks(diff_text, max_tokens=90000, max_part_tokens=None):
1195
+ """Splits the diff based on the token limit while preserving file header integrity.
1196
+
1197
+ - max_tokens: Diff size threshold that triggers Map-Reduce (default 90,000).
1198
+ - max_part_tokens: Maximum tokens per batch chunk (default 50,000).
1199
+ """
1200
+ if max_part_tokens is None:
1201
+ max_part_tokens = 50000
1202
+ if max_part_tokens > max_tokens:
1203
+ max_part_tokens = max_tokens
1204
+
1104
1205
  if estimate_token_count(diff_text) <= max_tokens:
1105
1206
  return [diff_text]
1106
1207
 
1107
- parts = re.split(r"(^diff --git a/)", diff_text, flags=re.MULTILINE)
1108
- chunks = []
1109
- current_chunk = ""
1208
+ parts = re.split(r"(^diff --git (?:[abciwos12]/|\S+ ))", diff_text, flags=re.MULTILINE)
1209
+ if len(parts) <= 1:
1210
+ return _split_line_chunk(diff_text, max_part_tokens)
1110
1211
 
1212
+ file_diffs = []
1213
+ if parts[0].strip():
1214
+ file_diffs.append(parts[0])
1111
1215
  for i in range(1, len(parts), 2):
1112
- file_diff = parts[i] + parts[i + 1] if i + 1 < len(parts) else parts[i]
1216
+ file_d = parts[i] + (parts[i + 1] if i + 1 < len(parts) else "")
1217
+ file_diffs.append(file_d)
1218
+
1219
+ chunks = []
1220
+ current_chunk = ""
1113
1221
 
1114
- if (
1115
- estimate_token_count(current_chunk + file_diff) > max_tokens
1222
+ for file_diff in file_diffs:
1223
+ file_tokens = estimate_token_count(file_diff)
1224
+ if file_tokens > max_part_tokens:
1225
+ if current_chunk:
1226
+ chunks.append(current_chunk)
1227
+ current_chunk = ""
1228
+ file_sub_chunks = _split_giant_file(file_diff, max_part_tokens)
1229
+ chunks.extend(file_sub_chunks)
1230
+ elif (
1231
+ estimate_token_count(current_chunk + file_diff) > max_part_tokens
1116
1232
  and current_chunk
1117
1233
  ):
1118
1234
  chunks.append(current_chunk)
@@ -1369,7 +1485,11 @@ def generate_pr_content(
1369
1485
  fg="cyan",
1370
1486
  )
1371
1487
 
1372
- chunks = split_diff_into_chunks(diff_text, max_tokens=90000)
1488
+ from src.config import get_map_reduce_thresholds
1489
+ max_tokens, max_part_tokens = get_map_reduce_thresholds(provider)
1490
+ chunks = split_diff_into_chunks(
1491
+ diff_text, max_tokens=max_tokens, max_part_tokens=max_part_tokens
1492
+ )
1373
1493
  total_meta = {
1374
1494
  "prompt_tokens": 0,
1375
1495
  "completion_tokens": 0,
@@ -1479,6 +1599,70 @@ def generate_pr_content(
1479
1599
  )
1480
1600
  diff_unificado = "\n\n".join(resumos_parciais)
1481
1601
 
1602
+ if estimate_token_count(diff_unificado) > max_tokens:
1603
+ click.secho(
1604
+ __("🔄 Large summary set detected! Running intermediate reduction..."),
1605
+ fg="cyan",
1606
+ dim=True,
1607
+ )
1608
+ intermediate_summaries = []
1609
+ curr_batch = []
1610
+ curr_tokens = 0
1611
+ for item in resumos_parciais:
1612
+ item_tok = estimate_token_count(item)
1613
+ if curr_tokens + item_tok > max_part_tokens and curr_batch:
1614
+ batch_str = "\n\n".join(curr_batch)
1615
+ inter_prompt = (
1616
+ __(
1617
+ "Generate ONLY a JSON object in the format {json_format} consolidating these technical summaries into a unified summary:\n",
1618
+ json_format='{"resumo": "..."}',
1619
+ )
1620
+ + batch_str
1621
+ )
1622
+ inter_res = call_ai_model(
1623
+ provider,
1624
+ api_key,
1625
+ api_model,
1626
+ inter_prompt,
1627
+ instrucao_sistema,
1628
+ quiet=True,
1629
+ action=f"{action_folder}_inter_reduce",
1630
+ )
1631
+ if inter_res and "resumo" in inter_res:
1632
+ intermediate_summaries.append(inter_res["resumo"])
1633
+ _aggregate_meta(inter_res.pop("_telemetry_meta", None))
1634
+ else:
1635
+ intermediate_summaries.append(batch_str)
1636
+ curr_batch = [item]
1637
+ curr_tokens = item_tok
1638
+ else:
1639
+ curr_batch.append(item)
1640
+ curr_tokens += item_tok
1641
+ if curr_batch:
1642
+ batch_str = "\n\n".join(curr_batch)
1643
+ inter_prompt = (
1644
+ __(
1645
+ "Generate ONLY a JSON object in the format {json_format} consolidating these technical summaries into a unified summary:\n",
1646
+ json_format='{"resumo": "..."}',
1647
+ )
1648
+ + batch_str
1649
+ )
1650
+ inter_res = call_ai_model(
1651
+ provider,
1652
+ api_key,
1653
+ api_model,
1654
+ inter_prompt,
1655
+ instrucao_sistema,
1656
+ quiet=True,
1657
+ action=f"{action_folder}_inter_reduce",
1658
+ )
1659
+ if inter_res and "resumo" in inter_res:
1660
+ intermediate_summaries.append(inter_res["resumo"])
1661
+ _aggregate_meta(inter_res.pop("_telemetry_meta", None))
1662
+ else:
1663
+ intermediate_summaries.append(batch_str)
1664
+ diff_unificado = "\n\n".join(intermediate_summaries)
1665
+
1482
1666
  if action_type == "commit":
1483
1667
  prompt = (
1484
1668
  __(
@@ -70,10 +70,13 @@ def resolve_scm_provider(config: dict) -> ScmProvider:
70
70
  def detect_provider_from_remote(remote_url: str, default: Optional[str] = "github") -> Optional[str]:
71
71
  """Guess the forge from a git remote URL (case-insensitive substrings).
72
72
 
73
- Order matters only for URLs that match several hosts — none exist today.
74
- Anything unknown returns *default*: "github" reproduces the historical
75
- behavior, while ``default=None`` lets a caller tell "recognised as GitHub"
76
- apart from "not recognised at all" (self-hosted hosts need the latter).
73
+ The named forges are checked first, so a URL that mentions one of them wins
74
+ even when it also contains "github". Anything unknown returns *default*:
75
+ "github" reproduces the historical behavior, while ``default=None`` lets a
76
+ caller tell "recognised as GitHub" apart from "not recognised at all"
77
+ (self-hosted hosts need the latter). For that reason the GitHub check runs
78
+ before the ``None`` return: ``github.com`` is a recognised forge, not an
79
+ unknown host.
77
80
  """
78
81
  url = (remote_url or "").lower()
79
82
  if "gitlab" in url:
@@ -82,10 +85,10 @@ def detect_provider_from_remote(remote_url: str, default: Optional[str] = "githu
82
85
  return "bitbucket"
83
86
  if "dev.azure.com" in url or "visualstudio.com" in url:
84
87
  return "azure_devops"
85
- if default is None:
86
- return None
87
88
  if "github" in url:
88
89
  return "github"
90
+ if default is None:
91
+ return None
89
92
  return default
90
93
 
91
94
 
src/issue_engine.py CHANGED
@@ -170,7 +170,12 @@ def generate_issue_content(context_text, context_type="diff"):
170
170
  total_meta["duration_ms"] += new_meta.get("duration_ms", 0)
171
171
 
172
172
  if context_type == "diff":
173
- chunks = split_diff_into_chunks(context_text, max_tokens=90000)
173
+ from src.config import get_map_reduce_thresholds
174
+
175
+ max_tokens, max_part_tokens = get_map_reduce_thresholds(provider)
176
+ chunks = split_diff_into_chunks(
177
+ context_text, max_tokens=max_tokens, max_part_tokens=max_part_tokens
178
+ )
174
179
  else:
175
180
  chunks = [context_text]
176
181
 
src/review/remote_pr.py CHANGED
@@ -342,9 +342,15 @@ def _chunk_count(diff_text):
342
342
  back, and the callers that need it (the MCP tool especially) have no
343
343
  terminal to read the "processing in N batches" line from.
344
344
  """
345
+ from src.config import get_map_reduce_thresholds
345
346
  from src.core import split_diff_into_chunks
346
347
 
347
348
  try:
348
- return len(split_diff_into_chunks(diff_text, max_tokens=90000))
349
+ max_tokens, max_part_tokens = get_map_reduce_thresholds()
350
+ return len(
351
+ split_diff_into_chunks(
352
+ diff_text, max_tokens=max_tokens, max_part_tokens=max_part_tokens
353
+ )
354
+ )
349
355
  except Exception:
350
356
  return 1
src/scm_settings.py CHANGED
@@ -366,6 +366,13 @@ def _ignored_provider_warning(choice) -> str:
366
366
  )
367
367
 
368
368
 
369
+ def _default_provider_warning() -> str:
370
+ return __(
371
+ "⚠️ The origin remote does not name a forge, so GitPR is using GitHub. "
372
+ "Run 'gitpr --init' to choose the right forge for this repository."
373
+ )
374
+
375
+
369
376
  def resolve_scm_settings(
370
377
  start_dir=None, remote_url=None, config_path=None, warnings=None
371
378
  ) -> ScmSettings:
@@ -390,6 +397,11 @@ def resolve_scm_settings(
390
397
  choice = resolve_provider_choice(project_file.provider, remote)
391
398
  if choice.ignored:
392
399
  collected.append(_ignored_provider_warning(choice))
400
+ if remote and choice.source == SOURCE_DEFAULT:
401
+ # An existing remote that names no forge: the repository is about to be
402
+ # driven as GitHub unless someone records an override. The absent-remote
403
+ # case has its own error at the call sites.
404
+ collected.append(_default_provider_warning())
393
405
 
394
406
  token = _token_for(choice.key)
395
407
  if os.getenv(RAW_TOKEN_KEY):
src/updater.py CHANGED
@@ -9,8 +9,8 @@ from packaging.version import InvalidVersion, Version
9
9
  # Canonical Single Source of Truth (SSOT) for GitPR package version (PyPI distribution)
10
10
  # ── Must stay ABOVE the i18n import: i18n.py lazily imports __lang_version__
11
11
  # from here, and pyproject.toml reads __version__ via setuptools attr:.
12
- __version__ = "1.7.0" # GitPR current package version (Canonical SSOT)
13
- __lang_version__ = "v0.0.42" # Language dictionary version control (asset, independent of package version)
12
+ __version__ = "1.8.0" # GitPR current package version (Canonical SSOT)
13
+ __lang_version__ = "v0.0.43" # Language dictionary version control (asset, independent of package version)
14
14
  __scripts_version__ = (
15
15
  "v0.0.4" # Git Hook scripts version control (independent from __lang_version__)
16
16
  )