gitpr-cli 1.1.0__py3-none-any.whl → 1.2.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.
- {gitpr_cli-1.1.0.dist-info → gitpr_cli-1.2.0.dist-info}/METADATA +9 -5
- gitpr_cli-1.2.0.dist-info/RECORD +67 -0
- src/cache.py +63 -2
- src/config.py +52 -0
- src/config_schema.py +60 -0
- src/core.py +49 -4
- src/diff_parser.py +162 -0
- src/fix/__init__.py +16 -0
- src/fix/apply_fix.py +479 -0
- src/fix/fix_history.py +153 -0
- src/fix/patch_applier.py +172 -0
- src/fix/patch_extractor.py +61 -0
- src/fix/patch_provenance.py +96 -0
- src/fix/patch_safety_classifier.py +101 -0
- src/fix/rollback_fix.py +69 -0
- src/infrastructure/scm/azure_devops_provider.py +13 -0
- src/infrastructure/scm/base.py +29 -1
- src/infrastructure/scm/bitbucket_provider.py +9 -0
- src/infrastructure/scm/github_provider.py +77 -4
- src/infrastructure/scm/gitlab_provider.py +50 -3
- src/linter_engine.py +11 -2
- src/main.py +469 -56
- src/mcp_server.py +263 -0
- src/review/__init__.py +13 -0
- src/review/diff_normalizer.py +84 -0
- src/review/diff_source.py +69 -0
- src/review/remote_pr.py +319 -0
- src/review/render.py +75 -0
- src/reviewer_resolution.py +159 -0
- src/reviewer_suggestion.py +19 -3
- src/suggest_reviewers.py +19 -12
- src/ui/chat_app.py +3 -31
- src/ui/pr_publish_app.py +222 -38
- src/updater.py +2 -2
- gitpr_cli-1.1.0.dist-info/RECORD +0 -53
- {gitpr_cli-1.1.0.dist-info → gitpr_cli-1.2.0.dist-info}/WHEEL +0 -0
- {gitpr_cli-1.1.0.dist-info → gitpr_cli-1.2.0.dist-info}/entry_points.txt +0 -0
- {gitpr_cli-1.1.0.dist-info → gitpr_cli-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {gitpr_cli-1.1.0.dist-info → gitpr_cli-1.2.0.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitpr-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.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
|
|
@@ -148,11 +148,11 @@ You can pass the following *flags* for specific actions:
|
|
|
148
148
|
* `--status`: Lists uncommitted file changes categorized as **new**, **modified**, and **deleted** — fast, no AI, no network. 📖 [Full docs](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/git-status.md)
|
|
149
149
|
* `--no-unstaged-check`: Skips the unstaged files verification before AI processing for a single invocation. Equivalent to `GITPR_SKIP_UNSTAGED_CHECK=true` for one run. 📖 [Full docs](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/git-status.md)
|
|
150
150
|
* `--linter-setup`: **Interactive external linter wizard.** Guides you through installing and configuring external linters (ESLint, PHPCS, Stylelint) as a Checkstyle XML bridge. 📖 [Full docs](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/linter-regras-customizadas.md)
|
|
151
|
-
* `--mcp`: Starts GitPR as an **MCP server** (Model Context Protocol) on stdio transport. Enables integration with VS Code, Cursor, Claude Desktop, and other MCP-compatible editors — exposing all GitPR AI capabilities as
|
|
151
|
+
* `--mcp`: Starts GitPR as an **MCP server** (Model Context Protocol) on stdio transport. Enables integration with VS Code, Cursor, Claude Desktop, and other MCP-compatible editors — exposing all GitPR AI capabilities as 14 annotated tools, 18 resources, and 7 pre-built prompts directly inside your IDE. Also available as the standalone `gitpr-mcp` command.
|
|
152
152
|
* `--plugins`: Lists all **globally installed plugins** — custom linter packs from `~/.gitpr/plugins/linter/` and MCP prompt templates from `~/.gitpr/plugins/prompts/`. These plugins apply across all your projects without duplication. 📖 [Full docs](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/plugins-system.md)
|
|
153
153
|
* `--install`: **Interactive Setup Wizard.** Runs a guided 4-step setup: downloads skill templates, installs Git hooks, configures MCP for detected editors, and checks/requests your AI provider API key. Each step asks for confirmation before proceeding.
|
|
154
154
|
* `-ih` or `--installhooks`: Automatically installs **local Git Hooks** (`pre-commit` and `prepare-commit-msg`) in your repository.
|
|
155
|
-
* `-s` or `--skill`: Creates the AI context template files (`.gitpr.commit.md`, `.gitpr.pr.md`, `.gitpr.review.md`, `.gitpr.filereview.md`, `.gitpr.issue.md`, `.gitpr.blame.md`) and the Linter (`.gitpr.linter.yml`)
|
|
155
|
+
* `-s` or `--skill`: Creates the AI context template files (`.gitpr.commit.md`, `.gitpr.pr.md`, `.gitpr.review.md`, `.gitpr.filereview.md`, `.gitpr.issue.md`, `.gitpr.blame.md`) and the Linter (`.gitpr.linter.yml`) inside `.gitpr/skill/`.
|
|
156
156
|
* `-is` or `--issue`: Automatically generates a draft of a **standardized Issue** and opens an interactive interface (TUI) for editing or direct submission via REST API. This feature has **3 context engines** depending on the command combination:
|
|
157
157
|
* **New Code Issue (`gitpr -is`):** Reads the current `git diff`. **Why use:** Ideal for quickly documenting the task you just finished programming, before committing.
|
|
158
158
|
* **Epic/Release Issue (`gitpr -is -ht`):** Reads the full history of the current branch (Git Log + PR Cache). **Why use:** Ideal for generating consolidated documentation of an entire release or a large *feature* that took several days/commits to complete.
|
|
@@ -229,7 +229,7 @@ You can dynamically switch models by configuring the `GEMINI_API_MODEL_PRIMARY`
|
|
|
229
229
|
|
|
230
230
|
## 🎯 Customizable "Skills" System (Prompt Engineering)
|
|
231
231
|
|
|
232
|
-
Instead of hiding AI instructions in the source code, GitPR uses local Markdown files that act as *System Instructions*. When running `gitpr -s`, the following files are generated
|
|
232
|
+
Instead of hiding AI instructions in the source code, GitPR uses local Markdown files that act as *System Instructions*. When running `gitpr -s`, the following files are generated inside `.gitpr/skill/` to customize the AI's "persona" according to your company's business rules:
|
|
233
233
|
|
|
234
234
|
* `.gitpr.commit.md`: Rules for generating short commit messages.
|
|
235
235
|
* `.gitpr.pr.md`: Required topic structure for the Pull Request description.
|
|
@@ -335,6 +335,8 @@ Once configured, use natural language in your editor's AI chat:
|
|
|
335
335
|
| `generate_issue` | Structured issue from diff, history, or blame |
|
|
336
336
|
| `list_unstaged_files` | Uncommitted file changes categorized (new/modified/deleted) |
|
|
337
337
|
| `analyze_unstaged_diff` | Unstaged diff only (working tree vs index) |
|
|
338
|
+
| `list_fix_candidates` | Fix candidates of the last review: patch, classification, id (read-only) |
|
|
339
|
+
| `review_remote_pr` | AI review of a pull request already open on the forge, fetched by number (read-only) |
|
|
338
340
|
|
|
339
341
|
### Direct CLI Invocation
|
|
340
342
|
|
|
@@ -430,12 +432,14 @@ If you want to implement GitPR as an automated quality barrier in your team, che
|
|
|
430
432
|
|
|
431
433
|
* [**Pull Request (Default Mode)**](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/pr-descricao-padrao.md) — Complete flow for generating PR descriptions without flags.
|
|
432
434
|
* [**Pull Request Publisher TUI**](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/pull-request-publication.md) — How to review and publish Pull Requests directly to GitHub from the terminal.
|
|
433
|
-
* [**AI Code Review**](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/code-review-ia.md) — Guide to review modes (`--review`, `--fullreview`)
|
|
435
|
+
* [**AI Code Review**](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/code-review-ia.md) — Guide to review modes (`--review`, `--fullreview`), file auditing (`--input`) and remote pull request review (`gitpr review-pr`).
|
|
436
|
+
* [**Remote Pull Request Review (gitpr review-pr)**](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/review-pr.md) — How the `gitpr review-pr` subcommand reviews a pull request that is already open on the forge, fetching its diff from the API without checking out its branch.
|
|
434
437
|
* [**AI Commit Messages**](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/commit-message-ia.md) — How to generate messages in the Conventional Commits standard and integrate with Git Hooks.
|
|
435
438
|
* [**Issue Generation and TUI Interface**](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/issue-tui-help.md) — How to use the terminal graphical interface (TUI) and the 3 context engines to manage structured Issues.
|
|
436
439
|
* [**Code Archaeologist (Git Blame)**](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/blame-arqueologo.md) — How to trace the origin of business rules with `git blame` and AI.
|
|
437
440
|
* [**Skills and Templates System**](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/skill-template.md) — How to customize AI behavior with `.gitpr.*.md` files.
|
|
438
441
|
* [**Release Notes & Changelog (gitpr release)**](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/release-notes.md) — How the `gitpr release` subcommand generates the changelog / release notes of a repository, suggests the next semantic version and publishes releases on the forge.
|
|
442
|
+
* [**Fix Command (gitpr fix)**](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/fix-command.md) — How the `gitpr fix` subcommand turns the findings of the last review into patches you read before they touch your tree, classifies each one by safety, and undoes an applied patch on demand.
|
|
439
443
|
|
|
440
444
|
### Configuration & Infrastructure
|
|
441
445
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
gitpr_cli-1.2.0.dist-info/licenses/LICENSE,sha256=UQzLC6mwqb-ZNDkBBAqOxC-SMQcEID0gaM6oFcu6u48,27030
|
|
2
|
+
src/__init__.py,sha256=WZW9x8P5CYnru29UiarFa8q1nQzuwYWyIodSOlruCWI,29
|
|
3
|
+
src/ai_providers.py,sha256=QyFEOsXyA59lZziO5bl-4nlYFJpxBcxU9aLhcTBnkWE,14633
|
|
4
|
+
src/blame_engine.py,sha256=Zov1hxrt7Dcmf56ovJKz3QmHIfzDKtSub2iItXeg9-A,17104
|
|
5
|
+
src/cache.py,sha256=GC2ZT0ThAtH22aRRZapIeauATv_nqBfiRsXjqeMhAiI,6368
|
|
6
|
+
src/changelog_builder.py,sha256=6tqJVHpY4Ta5shOSNWDEo_eHlKmDrU2dUSzS0wyYe10,7116
|
|
7
|
+
src/chat_memory.py,sha256=PcohnyspAfIwwwbsp9U2Wk_1AylJMFEMoWqRSJe_w30,8949
|
|
8
|
+
src/commit_classifier.py,sha256=8_LHQEzisWflV5ZsK31P3fwyu2QWacv8cdFErXYe030,4668
|
|
9
|
+
src/config.py,sha256=9945b0sTM5xL4a81M-CyJ6qfnAQ-Vkf3vr9Old79bjw,33985
|
|
10
|
+
src/config_schema.py,sha256=a7fSsp39h9Pe97uMrUTncuLjGzB8mWvF77vYhJZIcmw,35283
|
|
11
|
+
src/core.py,sha256=zWrz86ifjly5DqoqfQvqdQ0up6yyqFlDYYdB8t8cnyU,76310
|
|
12
|
+
src/diff_parser.py,sha256=62X69ves4tlzNgVKXNsfWdOMwqn2h16RkUOPqszMU7k,11252
|
|
13
|
+
src/doc_links.py,sha256=DuovU-RF9kjbisBdEoGAsuavQ2y0MYDhqy6Sz6vbrGI,1157
|
|
14
|
+
src/github_api.py,sha256=ILm7e03UP80swc3XuJvIJJvhLp81-V_3kQQ1EuAcN8Q,3836
|
|
15
|
+
src/i18n.py,sha256=8s-Bsvp-p3ZZhj0FvDNTRqfWsEio5v4VvXmS1NSqNW8,4578
|
|
16
|
+
src/issue_engine.py,sha256=XwW9C5uX84eOWWZMerm0DvE8LpLRcjhrLsXtt7yftak,10872
|
|
17
|
+
src/linter_engine.py,sha256=8SIs_FUtxEBeTMKWuqatqzlyyToYIjG5xeg0X7FYRAA,12683
|
|
18
|
+
src/linter_wizard.py,sha256=7J_nQFghVwCaKG6QI3K45KcOaxHSH1oTCAvP_6tnCQM,7424
|
|
19
|
+
src/main.py,sha256=B9X4Yr1QZxCioL2dvXOkJ7m37DzOR6DB2E6unLBqHuI,98535
|
|
20
|
+
src/mcp_server.py,sha256=q2clMxTtjLLC-P6Vh4XMq0xylnoUw-K2IFQvgvHIf7o,90479
|
|
21
|
+
src/metrics.py,sha256=VC68t8xGUBFry-ANvEb_TjaRkboPi37__GxK0eHZOe4,21250
|
|
22
|
+
src/net.py,sha256=f0MOkXyIxi4HVetsybtWpV_yHjiC17T2JMWySBdoV2I,2774
|
|
23
|
+
src/release_engine.py,sha256=gwTnOq0ESK26BqsW1RLlysGwL0ZS5guj1qG2V-yUyWI,21610
|
|
24
|
+
src/reviewer_resolution.py,sha256=lZ3csSJHN6Y5TJ3U3p9uuXikOgG7fH4qqx7X9-4F7pc,5754
|
|
25
|
+
src/reviewer_suggestion.py,sha256=jk9FAyp7WkkJbi-yepz7LDF1_tSHxJEEjcajMKF5HdY,8501
|
|
26
|
+
src/security.py,sha256=fkWy-r4tdISH0xOyWZyR8ZCFyLuYqu8fn9sKsIeNAso,1351
|
|
27
|
+
src/spinner.py,sha256=63pEotiDa3TVwBr-sn0MJoDPQ_uWJTTwINXieJIDDKc,8964
|
|
28
|
+
src/suggest_reviewers.py,sha256=-wRe0HYksx4FWQZ-eDvU9Ghdk5gqtEt457KQP7sE31Y,5181
|
|
29
|
+
src/tui_issue.py,sha256=WEFxT4kzWQ_KbSDsOnS_iuYXasZySB-68jryGNm0peg,10331
|
|
30
|
+
src/updater.py,sha256=hm4brG_lSPe82LWFVb41iVMOBYUI824svwsfzMfxxUQ,4771
|
|
31
|
+
src/usage_log.py,sha256=314sTT5Gl_uOXqWSIi-V42bnTHuMSYAZ6G6-_15uEGQ,5968
|
|
32
|
+
src/version_bump.py,sha256=OjqJe9OL1-1dT5od7-SgU5ZN_dTtR_aKX_oSjSY64YQ,2954
|
|
33
|
+
src/fix/__init__.py,sha256=YcN1nGpzWyXczd35LWEXdFpviKKsb9JS8e4bD6KcVcQ,935
|
|
34
|
+
src/fix/apply_fix.py,sha256=yMinMW50Fw62V-k7hRwN1lsikCJbz6xV5iX4IH0sdxY,17912
|
|
35
|
+
src/fix/fix_history.py,sha256=pHXHpt-SstitbLDmlPloDVKlH79FUD8hqKjVK5xKLMA,5626
|
|
36
|
+
src/fix/patch_applier.py,sha256=ahruhdKOhrt2excc3nJljoWi4_0pUj26owSVAQWeeKQ,6258
|
|
37
|
+
src/fix/patch_extractor.py,sha256=gEiwSvxGNHM74TnubTN22NBpbXDwvNpB2vMqgpF7hQQ,2514
|
|
38
|
+
src/fix/patch_provenance.py,sha256=sF934ntcZjJCTzzhtuR2Pi-vMUdXFElKcuw5riwPnUk,3195
|
|
39
|
+
src/fix/patch_safety_classifier.py,sha256=StZvh_0Y-ZCdf_LwFoVAdfhpi0b7v1TOxTRoG86_63M,4121
|
|
40
|
+
src/fix/rollback_fix.py,sha256=rMBYewuvlhJOFdo-Ke_SPfu1O-yd6NJzEYiO5o-LcKk,2501
|
|
41
|
+
src/infrastructure/__init__.py,sha256=zTIcY3cscsdkXogSGXoJbH0mVGFbKYTRK32fg26wr9U,81
|
|
42
|
+
src/infrastructure/scm/__init__.py,sha256=3_4CmrZPZSmDbFwf_0YBeKc935jWV8oAHaZtC_i-Fak,1550
|
|
43
|
+
src/infrastructure/scm/azure_devops_provider.py,sha256=ezRS_O2450mfYEfqFOD4UBSVFhT3aJdx3DEVkZcJjac,14717
|
|
44
|
+
src/infrastructure/scm/base.py,sha256=L-7NYG2hqIWKzKCsQ5NurtgJJvvYvjTVOs0NJ2Aqx_M,9536
|
|
45
|
+
src/infrastructure/scm/bitbucket_provider.py,sha256=jv62MlOd0Iiu3VYAHUffJO2D95TE4k2lmukQL93bOd8,12105
|
|
46
|
+
src/infrastructure/scm/factory.py,sha256=Qeearq8eWnF_4boI68rVBkoGEiS-xRGD4V-O_FF0204,3856
|
|
47
|
+
src/infrastructure/scm/github_provider.py,sha256=9RXRAocYEVWS6NR2PsOg7wtqiF8989x1BxAqgBSoLOI,16438
|
|
48
|
+
src/infrastructure/scm/gitlab_provider.py,sha256=mznhtBhXtc4zRut_Jb0pM57RIXViNYYiKiM7VKDFhl8,13881
|
|
49
|
+
src/review/__init__.py,sha256=XVyQZFf6UAqIKi8Gb1pQ4dKjcrC50soUvNuKu1DBspc,687
|
|
50
|
+
src/review/diff_normalizer.py,sha256=PRXQ7b81EBH1nx_8_Tm9G7Kjdax_6hIVtFgEixicT0E,3551
|
|
51
|
+
src/review/diff_source.py,sha256=O8bIlff0Zr7wPMzCKAVk_LF2pMDr3sc9KAuyA3OvHG0,2664
|
|
52
|
+
src/review/remote_pr.py,sha256=maPCQO5p7u9-_PA0FEBNgimfA6na2BdQGBPmqNj1_0c,11817
|
|
53
|
+
src/review/render.py,sha256=Hnkz0PvtYIaaikTS1inTzrneAJL4Xqjd-F8Lr6Yh6mQ,2691
|
|
54
|
+
src/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
55
|
+
src/ui/chat_app.py,sha256=6qpjWxHECYbKc5PTfbr63FjGo0j5-gXf3X-qW1klWBg,25396
|
|
56
|
+
src/ui/config_app.py,sha256=xgppZbEBFrFijgLGo1b1PN1vTGCg8xpI8gpMYBv2KzU,73417
|
|
57
|
+
src/ui/help_screen.py,sha256=EvbP8kFZ6Sdehi7bJ6Wo_YcTTk5MTkgneeidWwefYrA,1890
|
|
58
|
+
src/ui/issue_app.py,sha256=eINMmamloeopbtwyS1rCPwosaHeZW5oLebnEV4TfamY,9281
|
|
59
|
+
src/ui/linter_app.py,sha256=Y5vwU7D5PQIR_HaY6t40jZy7Guc8z2w-auyosCQqoro,1566
|
|
60
|
+
src/ui/metrics_app.py,sha256=a3jsB_kjMusSL57A6LpJyb9LFN7JJWlOdUNClklwq-c,16016
|
|
61
|
+
src/ui/pr_publish_app.py,sha256=cZ9xr1TSZF6fjvQPlAdSjzSNykufHF1l7hsmo3Ib2j0,67311
|
|
62
|
+
src/ui/pr_publish_help.py,sha256=fBiUtD97ngxbSfAvPVGGXU19r5kbALCKyi4QeKqyQ2c,1891
|
|
63
|
+
gitpr_cli-1.2.0.dist-info/METADATA,sha256=1BSAiMJtMgY7j3YsU0YgaHw7rpfk2ccj5z2iOzSjyxk,34689
|
|
64
|
+
gitpr_cli-1.2.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
65
|
+
gitpr_cli-1.2.0.dist-info/entry_points.txt,sha256=TstGnMUzdOI5qNmpM97EPDBPuMqOZxoo6EoLxHSOMSQ,71
|
|
66
|
+
gitpr_cli-1.2.0.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
|
|
67
|
+
gitpr_cli-1.2.0.dist-info/RECORD,,
|
src/cache.py
CHANGED
|
@@ -47,9 +47,22 @@ def get_cached_response(action_folder, prompt_text):
|
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
def save_cached_response(
|
|
50
|
-
action_folder,
|
|
50
|
+
action_folder,
|
|
51
|
+
action_type,
|
|
52
|
+
prompt_text,
|
|
53
|
+
response_dict,
|
|
54
|
+
meta_raw=None,
|
|
55
|
+
reviewed_diff=None,
|
|
51
56
|
):
|
|
52
|
-
"""Saves the AI response to the local cache.
|
|
57
|
+
"""Saves the AI response to the local cache.
|
|
58
|
+
|
|
59
|
+
``reviewed_diff`` is the diff the review actually ran on. When given it is
|
|
60
|
+
stored at the top level of the record, so ``gitpr fix`` can re-derive its
|
|
61
|
+
patch from the revision that was reviewed instead of whatever the working
|
|
62
|
+
tree happens to hold later — the two differ whenever the review came from a
|
|
63
|
+
pull request (src/review/remote_pr.py) or from a full-branch diff. Records
|
|
64
|
+
written before this field existed simply lack it, and the reader falls back.
|
|
65
|
+
"""
|
|
53
66
|
md5_hash = generate_md5(prompt_text)
|
|
54
67
|
folder_path = get_cache_base_dir() / action_folder
|
|
55
68
|
folder_path.mkdir(parents=True, exist_ok=True)
|
|
@@ -76,6 +89,9 @@ def save_cached_response(
|
|
|
76
89
|
"response": response_dict,
|
|
77
90
|
}
|
|
78
91
|
|
|
92
|
+
if reviewed_diff is not None:
|
|
93
|
+
cache_data["diff"] = reviewed_diff
|
|
94
|
+
|
|
79
95
|
try:
|
|
80
96
|
with open(cache_file, "w", encoding="utf-8") as f:
|
|
81
97
|
json.dump(cache_data, f, indent=2, ensure_ascii=False)
|
|
@@ -83,6 +99,51 @@ def save_cached_response(
|
|
|
83
99
|
pass # Silent cache failure to avoid crashing the tool
|
|
84
100
|
|
|
85
101
|
|
|
102
|
+
# Review action types whose prompt is a branch diff. `filereview` is left out
|
|
103
|
+
# on purpose: it audits a single file and has no diff to re-derive.
|
|
104
|
+
REVIEW_ACTION_TYPES = ("review", "fullreview")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def resolve_last_review(repo_name, branch_name):
|
|
108
|
+
"""The most recent cached review for *repo_name* and *branch_name*, or None.
|
|
109
|
+
|
|
110
|
+
All three review modes write into the same ``review/`` folder (see
|
|
111
|
+
``core.generate_pr_content``), so ``action_type`` is what tells them apart —
|
|
112
|
+
without that filter a file audit would be mistaken for a branch review.
|
|
113
|
+
|
|
114
|
+
Returns the cache record itself: the caller reads
|
|
115
|
+
``record["response"]["review"]`` for the text and ``record["action_type"]``
|
|
116
|
+
to know which diff produced it, so the patch can be re-derived against
|
|
117
|
+
today's tree rather than the tree of the day the review ran.
|
|
118
|
+
"""
|
|
119
|
+
review_folder = get_cache_base_dir() / "review"
|
|
120
|
+
if not review_folder.exists():
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
newest = None
|
|
124
|
+
for cache_file in review_folder.glob("*.json"):
|
|
125
|
+
try:
|
|
126
|
+
with open(cache_file, "r", encoding="utf-8", errors="replace") as f:
|
|
127
|
+
data = json.load(f)
|
|
128
|
+
except (json.JSONDecodeError, IOError):
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
if not isinstance(data, dict):
|
|
132
|
+
continue
|
|
133
|
+
if data.get("repo") != repo_name or data.get("branch") != branch_name:
|
|
134
|
+
continue
|
|
135
|
+
if data.get("action_type") not in REVIEW_ACTION_TYPES:
|
|
136
|
+
continue
|
|
137
|
+
if not (data.get("response") or {}).get("review"):
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
# Timestamps are '%Y-%m-%d %H:%M:%S', so string order is date order.
|
|
141
|
+
if newest is None or data.get("datetime", "") > newest.get("datetime", ""):
|
|
142
|
+
newest = data
|
|
143
|
+
|
|
144
|
+
return newest
|
|
145
|
+
|
|
146
|
+
|
|
86
147
|
def get_cached_pr_descriptions(repo_name, branch_name):
|
|
87
148
|
"""Searches the cache for all historically generated PRs for this repository and branch."""
|
|
88
149
|
from src.i18n import __
|
src/config.py
CHANGED
|
@@ -61,6 +61,16 @@ DEFAULT_CONFIG = {
|
|
|
61
61
|
"GITPR_RELEASE_AI_SUMMARY": "true",
|
|
62
62
|
"GITPR_RELEASE_AUTO_BUMP": "true",
|
|
63
63
|
"GITPR_RELEASE_PUBLISH_DRAFT_BY_DEFAULT": "true",
|
|
64
|
+
# Fix feature (gitpr fix). The excluded paths are glob patterns a patch may
|
|
65
|
+
# not touch and still count as safe; they are about *risk*, not diff noise,
|
|
66
|
+
# which is why they are not shared with the smart-excludes list.
|
|
67
|
+
"GITPR_FIX_SAFE_MAX_LINES_CHANGED": "5",
|
|
68
|
+
"GITPR_FIX_SAFE_EXCLUDED_PATHS": (
|
|
69
|
+
"database/migrations/**;**/*.ci.yml;docker/**;terraform/**;.github/workflows/**"
|
|
70
|
+
),
|
|
71
|
+
"GITPR_FIX_REQUIRE_CONFIRMATION": "true",
|
|
72
|
+
"GITPR_FIX_CREATE_BRANCH_ON_ALL_SAFE": "true",
|
|
73
|
+
"GITPR_FIX_BRANCH_NAME_TEMPLATE": "fix/gitpr-{datetime}",
|
|
64
74
|
}
|
|
65
75
|
|
|
66
76
|
# Fallbacks used when the .env value is missing or not a positive number.
|
|
@@ -131,6 +141,7 @@ SKILL_FILES_BY_TYPE = {
|
|
|
131
141
|
"issue": ".gitpr.issue.md",
|
|
132
142
|
"blame": ".gitpr.blame.md",
|
|
133
143
|
"release": ".gitpr.release.md",
|
|
144
|
+
"fix": ".gitpr.fix.md",
|
|
134
145
|
}
|
|
135
146
|
SKILL_TYPES = tuple(SKILL_FILES_BY_TYPE)
|
|
136
147
|
|
|
@@ -680,6 +691,47 @@ def get_release_settings():
|
|
|
680
691
|
}
|
|
681
692
|
|
|
682
693
|
|
|
694
|
+
def get_fix_settings():
|
|
695
|
+
"""Returns the gitpr fix configuration as a flat dict.
|
|
696
|
+
|
|
697
|
+
``safe_excluded_paths`` is the ; separated list of glob patterns a patch may
|
|
698
|
+
not touch and still be classified safe; an empty value falls back to the
|
|
699
|
+
built-in list, because clearing the field by accident must not silently drop
|
|
700
|
+
the protection. ``branch_name_template`` names the branch of a batch with
|
|
701
|
+
the vocabulary the output filenames already use ({branch} and {datetime}),
|
|
702
|
+
so there is one placeholder syntax to learn.
|
|
703
|
+
"""
|
|
704
|
+
load_dotenv(ENV_FILE)
|
|
705
|
+
try:
|
|
706
|
+
max_lines = int(
|
|
707
|
+
(os.getenv("GITPR_FIX_SAFE_MAX_LINES_CHANGED") or "").strip() or 5
|
|
708
|
+
)
|
|
709
|
+
if max_lines <= 0:
|
|
710
|
+
raise ValueError
|
|
711
|
+
except ValueError:
|
|
712
|
+
max_lines = 5
|
|
713
|
+
|
|
714
|
+
# The fallback is spelled out, like every other getter here, instead of
|
|
715
|
+
# looked up in DEFAULT_CONFIG.
|
|
716
|
+
raw_paths = os.getenv("GITPR_FIX_SAFE_EXCLUDED_PATHS") or (
|
|
717
|
+
"database/migrations/**;**/*.ci.yml;docker/**;terraform/**;.github/workflows/**"
|
|
718
|
+
)
|
|
719
|
+
excluded_paths = tuple(
|
|
720
|
+
item.strip() for item in raw_paths.split(";") if item.strip()
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
template = (os.getenv("GITPR_FIX_BRANCH_NAME_TEMPLATE") or "").strip()
|
|
724
|
+
return {
|
|
725
|
+
"safe_max_lines_changed": max_lines,
|
|
726
|
+
"safe_excluded_paths": excluded_paths,
|
|
727
|
+
"require_confirmation": _env_bool_default_true("GITPR_FIX_REQUIRE_CONFIRMATION"),
|
|
728
|
+
"create_branch_on_all_safe": _env_bool_default_true(
|
|
729
|
+
"GITPR_FIX_CREATE_BRANCH_ON_ALL_SAFE"
|
|
730
|
+
),
|
|
731
|
+
"branch_name_template": template or "fix/gitpr-{datetime}",
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
|
|
683
735
|
def validate_github_token(token):
|
|
684
736
|
"""
|
|
685
737
|
Validates a GitHub PAT by making a lightweight API call to /user.
|
src/config_schema.py
CHANGED
|
@@ -180,6 +180,12 @@ CATEGORIES = (
|
|
|
180
180
|
__("Changelog generation and release publishing."),
|
|
181
181
|
doc="release-notes.md",
|
|
182
182
|
),
|
|
183
|
+
Category(
|
|
184
|
+
"fix",
|
|
185
|
+
__("Fix"),
|
|
186
|
+
__("Review findings turned into patches, and the safety rules of a batch."),
|
|
187
|
+
doc="fix-command.md",
|
|
188
|
+
),
|
|
183
189
|
Category(
|
|
184
190
|
"scm",
|
|
185
191
|
__("SCM / Forge"),
|
|
@@ -221,6 +227,7 @@ SKILL_LABELS = {
|
|
|
221
227
|
"issue": __("Issue"),
|
|
222
228
|
"blame": __("Blame"),
|
|
223
229
|
"release": __("Release"),
|
|
230
|
+
"fix": __("Fix"),
|
|
224
231
|
}
|
|
225
232
|
|
|
226
233
|
# Sub-headers shown inside a category. Generic words go through __(); forge and
|
|
@@ -614,6 +621,59 @@ FIELDS = (
|
|
|
614
621
|
kind=KIND_TEMPLATE,
|
|
615
622
|
default="{branch}_{datetime}_RELEASE.md",
|
|
616
623
|
),
|
|
624
|
+
# --------------------------------------------------------------------- Fix
|
|
625
|
+
ConfigField(
|
|
626
|
+
key="GITPR_FIX_SAFE_MAX_LINES_CHANGED",
|
|
627
|
+
label=__("Safe Patch Size"),
|
|
628
|
+
description=__(
|
|
629
|
+
"Maximum number of added plus removed lines a patch may have and still be classified as safe."
|
|
630
|
+
),
|
|
631
|
+
category="fix",
|
|
632
|
+
kind=KIND_INT,
|
|
633
|
+
default="5",
|
|
634
|
+
),
|
|
635
|
+
ConfigField(
|
|
636
|
+
key="GITPR_FIX_SAFE_EXCLUDED_PATHS",
|
|
637
|
+
label=__("Sensitive Paths"),
|
|
638
|
+
description=__(
|
|
639
|
+
"Semicolon separated glob patterns a patch may not touch and still be safe (migrations, workflows, containers, infrastructure)."
|
|
640
|
+
),
|
|
641
|
+
category="fix",
|
|
642
|
+
kind=KIND_STR,
|
|
643
|
+
default=(
|
|
644
|
+
"database/migrations/**;**/*.ci.yml;docker/**;terraform/**;.github/workflows/**"
|
|
645
|
+
),
|
|
646
|
+
),
|
|
647
|
+
ConfigField(
|
|
648
|
+
key="GITPR_FIX_REQUIRE_CONFIRMATION",
|
|
649
|
+
label=__("Ask Before Applying"),
|
|
650
|
+
description=__(
|
|
651
|
+
"Shows the diff and asks for confirmation before a patch is written to the working tree."
|
|
652
|
+
),
|
|
653
|
+
category="fix",
|
|
654
|
+
kind=KIND_BOOL,
|
|
655
|
+
default="true",
|
|
656
|
+
),
|
|
657
|
+
ConfigField(
|
|
658
|
+
key="GITPR_FIX_CREATE_BRANCH_ON_ALL_SAFE",
|
|
659
|
+
label=__("Branch for a Batch"),
|
|
660
|
+
description=__(
|
|
661
|
+
"Creates a new branch when the safe patches are applied in a batch, so the current branch stays untouched."
|
|
662
|
+
),
|
|
663
|
+
category="fix",
|
|
664
|
+
kind=KIND_BOOL,
|
|
665
|
+
default="true",
|
|
666
|
+
),
|
|
667
|
+
ConfigField(
|
|
668
|
+
key="GITPR_FIX_BRANCH_NAME_TEMPLATE",
|
|
669
|
+
label=__("Branch Name"),
|
|
670
|
+
description=__(
|
|
671
|
+
"Name of the branch created for a batch. Placeholders: branch and datetime."
|
|
672
|
+
),
|
|
673
|
+
category="fix",
|
|
674
|
+
kind=KIND_TEMPLATE,
|
|
675
|
+
default="fix/gitpr-{datetime}",
|
|
676
|
+
),
|
|
617
677
|
# --------------------------------------------------------------- SCM/Forge
|
|
618
678
|
# The common settings stay ungrouped and always visible; the fields that
|
|
619
679
|
# only exist for one forge live under its sub-header and follow the
|
src/core.py
CHANGED
|
@@ -330,6 +330,23 @@ def _load_docs_smart_excludes():
|
|
|
330
330
|
SMART_EXCLUDES = _load_smart_excludes() + _load_docs_smart_excludes()
|
|
331
331
|
|
|
332
332
|
|
|
333
|
+
def get_smart_exclude_patterns():
|
|
334
|
+
"""
|
|
335
|
+
Return the raw glob patterns behind SMART_EXCLUDES, without the prefix.
|
|
336
|
+
|
|
337
|
+
SMART_EXCLUDES is a list of git pathspecs (":(exclude)*.lock") because that
|
|
338
|
+
is what `git diff` takes on its command line. A diff fetched from a forge
|
|
339
|
+
API never passes through git, so the same exclusions have to be applied in
|
|
340
|
+
Python — this exposes the patterns both paths share, derived from the very
|
|
341
|
+
list the git command uses so the two can never drift apart.
|
|
342
|
+
|
|
343
|
+
GITPR_SKIP_SMART_EXCLUDES is honored upstream in the loaders, so an empty
|
|
344
|
+
SMART_EXCLUDES already yields an empty list here.
|
|
345
|
+
"""
|
|
346
|
+
prefix = ":(exclude)"
|
|
347
|
+
return [p[len(prefix):] for p in SMART_EXCLUDES if p.startswith(prefix)]
|
|
348
|
+
|
|
349
|
+
|
|
333
350
|
def _get_raw_docs_patterns():
|
|
334
351
|
"""
|
|
335
352
|
Returns raw glob patterns for documentation extensions (e.g. ['*.md', '*.txt']).
|
|
@@ -742,8 +759,30 @@ def split_diff_into_chunks(diff_text, max_tokens=90000):
|
|
|
742
759
|
return chunks
|
|
743
760
|
|
|
744
761
|
|
|
745
|
-
def generate_pr_content(
|
|
746
|
-
|
|
762
|
+
def generate_pr_content(
|
|
763
|
+
action_folder,
|
|
764
|
+
action_type,
|
|
765
|
+
diff_text,
|
|
766
|
+
provider="gemini",
|
|
767
|
+
cache_scope="",
|
|
768
|
+
store_diff=False,
|
|
769
|
+
):
|
|
770
|
+
"""Sends the diff to the AI using System Instruction and returns a parsed JSON.
|
|
771
|
+
|
|
772
|
+
The engine has no idea where ``diff_text`` came from and does not need one:
|
|
773
|
+
a local ``git diff`` and a pull request fetched over the API are the same
|
|
774
|
+
text by the time they arrive, so both review flows share this function.
|
|
775
|
+
|
|
776
|
+
``cache_scope`` is appended to the cache key only — never to the prompt the
|
|
777
|
+
model receives — so a review of PR 42 and a review of an identical local
|
|
778
|
+
diff cannot answer each other. The default "" leaves the MD5 of every
|
|
779
|
+
existing local review untouched, so switching a caller to DiffSource
|
|
780
|
+
invalidates no cache.
|
|
781
|
+
|
|
782
|
+
``store_diff`` records the reviewed diff alongside the response, which is
|
|
783
|
+
what lets ``gitpr fix`` patch against the revision that was reviewed rather
|
|
784
|
+
than the working tree of the day it runs.
|
|
785
|
+
"""
|
|
747
786
|
if not diff_text or not diff_text.strip():
|
|
748
787
|
click.secho(
|
|
749
788
|
__("⚠️ No diff found. Make some changes before running the command."),
|
|
@@ -870,7 +909,7 @@ def generate_pr_content(action_folder, action_type, diff_text, provider="gemini"
|
|
|
870
909
|
pass # Non-critical — never block the main flow for this metadata
|
|
871
910
|
|
|
872
911
|
# TRY TO RETRIEVE FROM CACHE
|
|
873
|
-
cached_data = get_cached_response(action_folder, prompt)
|
|
912
|
+
cached_data = get_cached_response(action_folder, prompt + cache_scope)
|
|
874
913
|
if cached_data:
|
|
875
914
|
click.secho(__("⚡ Response retrieved from local cache."), fg="green", dim=True)
|
|
876
915
|
from src.metrics import log_command_metric
|
|
@@ -1061,7 +1100,12 @@ def generate_pr_content(action_folder, action_type, diff_text, provider="gemini"
|
|
|
1061
1100
|
# SAVE TO CACHE AND RETURN
|
|
1062
1101
|
if result_json:
|
|
1063
1102
|
save_cached_response(
|
|
1064
|
-
action_folder,
|
|
1103
|
+
action_folder,
|
|
1104
|
+
action_type,
|
|
1105
|
+
prompt + cache_scope,
|
|
1106
|
+
result_json,
|
|
1107
|
+
meta_raw=total_meta,
|
|
1108
|
+
reviewed_diff=diff_text if store_diff else None,
|
|
1065
1109
|
)
|
|
1066
1110
|
# Fire-and-forget metric for successful AI-powered command
|
|
1067
1111
|
from src.metrics import log_command_metric
|
|
@@ -1200,6 +1244,7 @@ def generate_skill_template():
|
|
|
1200
1244
|
".gitpr.blame.md": f"gitpr.blame{lang_suffix}.md",
|
|
1201
1245
|
".gitpr.issue.md": f"gitpr.issue{lang_suffix}.md",
|
|
1202
1246
|
".gitpr.release.md": f"gitpr.release{lang_suffix}.md",
|
|
1247
|
+
".gitpr.fix.md": f"gitpr.fix{lang_suffix}.md",
|
|
1203
1248
|
}
|
|
1204
1249
|
|
|
1205
1250
|
success_count = 0
|
src/diff_parser.py
CHANGED
|
@@ -5,8 +5,14 @@ The PR flows diff with ``git diff -U1 -w -M -B <ancestor> -- <excludes>``
|
|
|
5
5
|
numbers of the added ('+') lines, per file — the numbers used to blame the
|
|
6
6
|
working tree. It never runs git and never re-applies excludes: it only
|
|
7
7
|
interprets the diff text it is given.
|
|
8
|
+
|
|
9
|
+
``summarize_patch()`` reads the same text a second way — as a patch a whole:
|
|
10
|
+
which files it touches, how many hunks, how many lines it adds and removes.
|
|
11
|
+
That is what the ``gitpr fix`` safety classifier scores, and it accepts the
|
|
12
|
+
looser shape an AI returns (no ``diff --git`` line, just ``---``/``+++``).
|
|
8
13
|
"""
|
|
9
14
|
import re
|
|
15
|
+
from dataclasses import dataclass
|
|
10
16
|
|
|
11
17
|
_HUNK_HEADER_RE = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@")
|
|
12
18
|
|
|
@@ -157,3 +163,159 @@ def parse_added_lines(diff_text):
|
|
|
157
163
|
for lines in added_by_file.values():
|
|
158
164
|
lines.sort()
|
|
159
165
|
return added_by_file
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@dataclass(frozen=True)
|
|
169
|
+
class PatchSummary:
|
|
170
|
+
"""What a unified diff touches, as the safety classifier needs to see it.
|
|
171
|
+
|
|
172
|
+
``files`` holds each file the patch mentions, in order of appearance, taken
|
|
173
|
+
from the new side — the 'diff --git' line when there is one (a deletion
|
|
174
|
+
keeps its name there, since its '+++ /dev/null' carries none), otherwise the
|
|
175
|
+
'+++ b/path' header. ``hunks`` counts well-formed '@@' headers, and
|
|
176
|
+
``added``/``removed`` count content lines inside those hunks, so headers and
|
|
177
|
+
the '\\ No newline' marker never count. ``removed_lines`` keeps the text of
|
|
178
|
+
each removed line — what lets a caller spot a deleted function call.
|
|
179
|
+
"""
|
|
180
|
+
files: tuple[str, ...]
|
|
181
|
+
hunks: int
|
|
182
|
+
added: int
|
|
183
|
+
removed: int
|
|
184
|
+
removed_lines: tuple[str, ...]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def summarize_patch(diff_text):
|
|
188
|
+
"""Return a PatchSummary for *diff_text* (see the dataclass docstring).
|
|
189
|
+
|
|
190
|
+
Pure like parse_added_lines: it interprets the text it is given and never
|
|
191
|
+
runs git. Text with no recognizable diff section yields an empty summary
|
|
192
|
+
rather than an error — the caller decides whether that means "no patch".
|
|
193
|
+
"""
|
|
194
|
+
files = []
|
|
195
|
+
hunks = 0
|
|
196
|
+
added = 0
|
|
197
|
+
removed = 0
|
|
198
|
+
removed_lines = []
|
|
199
|
+
in_hunk = False
|
|
200
|
+
|
|
201
|
+
for raw_line in diff_text.split("\n"):
|
|
202
|
+
line = raw_line.rstrip("\r")
|
|
203
|
+
|
|
204
|
+
if line.startswith("diff --git "):
|
|
205
|
+
_remember_file(files, _strip_diff_prefix(_header_path(line)))
|
|
206
|
+
in_hunk = False
|
|
207
|
+
continue
|
|
208
|
+
if line.startswith("+++ ") and not in_hunk:
|
|
209
|
+
path = _strip_diff_prefix(_header_path(line, marker="+++ "))
|
|
210
|
+
if path and path != "/dev/null":
|
|
211
|
+
_remember_file(files, path)
|
|
212
|
+
in_hunk = False
|
|
213
|
+
continue
|
|
214
|
+
if line.startswith("@@"):
|
|
215
|
+
if _HUNK_HEADER_RE.match(line):
|
|
216
|
+
hunks += 1
|
|
217
|
+
in_hunk = True
|
|
218
|
+
continue
|
|
219
|
+
|
|
220
|
+
if not in_hunk or line.startswith("\\ No newline"):
|
|
221
|
+
continue
|
|
222
|
+
if line.startswith("+"):
|
|
223
|
+
added += 1
|
|
224
|
+
elif line.startswith("-"):
|
|
225
|
+
removed += 1
|
|
226
|
+
removed_lines.append(line[1:])
|
|
227
|
+
|
|
228
|
+
return PatchSummary(
|
|
229
|
+
files=tuple(files),
|
|
230
|
+
hunks=hunks,
|
|
231
|
+
added=added,
|
|
232
|
+
removed=removed,
|
|
233
|
+
removed_lines=tuple(removed_lines),
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _remember_file(files, path):
|
|
238
|
+
"""Append *path* to *files* once, preserving appearance order."""
|
|
239
|
+
if path and path not in files:
|
|
240
|
+
files.append(path)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@dataclass(frozen=True)
|
|
244
|
+
class PatchSection:
|
|
245
|
+
"""One file's slice of a unified diff, with the path it belongs to.
|
|
246
|
+
|
|
247
|
+
``path`` is the new-side path (the b/ side), so a deletion keeps its name —
|
|
248
|
+
its own '+++ /dev/null' header carries none. ``text`` is the section
|
|
249
|
+
verbatim, headers included, so the pieces of a split diff concatenate back
|
|
250
|
+
into something the review engine can still read.
|
|
251
|
+
"""
|
|
252
|
+
path: str
|
|
253
|
+
text: str
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def split_patch_sections(diff_text):
|
|
257
|
+
"""Split *diff_text* into one PatchSection per file, in order.
|
|
258
|
+
|
|
259
|
+
The counterpart of summarize_patch for callers that must drop whole files
|
|
260
|
+
(the remote-PR smart-excludes filter): it needs each file's path next to its
|
|
261
|
+
own text, which no other helper here exposes. Accepts both header shapes
|
|
262
|
+
summarize_patch accepts — git's own output ('diff --git' plus a '---'/'+++'
|
|
263
|
+
pair per file) and the looser shape an AI returns (bare '---'/'+++' pairs).
|
|
264
|
+
Text before the first header is dropped, as are sections whose path never
|
|
265
|
+
resolves; a diff with no recognizable file yields [].
|
|
266
|
+
|
|
267
|
+
Pure, like its siblings: it interprets the text it is given, never runs git.
|
|
268
|
+
"""
|
|
269
|
+
lines = [raw.rstrip("\r") for raw in diff_text.split("\n")]
|
|
270
|
+
sections = []
|
|
271
|
+
start = None
|
|
272
|
+
path = ""
|
|
273
|
+
in_hunk = False
|
|
274
|
+
|
|
275
|
+
def flush(end):
|
|
276
|
+
if start is not None and path:
|
|
277
|
+
sections.append(
|
|
278
|
+
PatchSection(path=path, text="\n".join(lines[start:end]))
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
i = 0
|
|
282
|
+
while i < len(lines):
|
|
283
|
+
line = lines[i]
|
|
284
|
+
|
|
285
|
+
if line.startswith("diff --git "):
|
|
286
|
+
flush(i)
|
|
287
|
+
start, in_hunk = i, False
|
|
288
|
+
# The b/ side names the file on the new revision, and unlike the
|
|
289
|
+
# '+++ /dev/null' of a deletion it is always populated.
|
|
290
|
+
path = _strip_diff_prefix(_header_path(line))
|
|
291
|
+
i += 1
|
|
292
|
+
continue
|
|
293
|
+
|
|
294
|
+
# A '---' immediately followed by a '+++' is a file header pair. Inside
|
|
295
|
+
# a 'diff --git' section it is that section's own header; anywhere else
|
|
296
|
+
# it opens a new file, which is how the AI's looser patches arrive.
|
|
297
|
+
if (
|
|
298
|
+
line.startswith("--- ")
|
|
299
|
+
and i + 1 < len(lines)
|
|
300
|
+
and lines[i + 1].startswith("+++ ")
|
|
301
|
+
):
|
|
302
|
+
candidate = _strip_diff_prefix(_header_path(lines[i + 1], marker="+++ "))
|
|
303
|
+
if candidate == "/dev/null":
|
|
304
|
+
candidate = ""
|
|
305
|
+
already_open = start is not None and lines[start].startswith("diff --git ")
|
|
306
|
+
if not already_open:
|
|
307
|
+
flush(i)
|
|
308
|
+
start, in_hunk = i, False
|
|
309
|
+
path = candidate
|
|
310
|
+
elif candidate:
|
|
311
|
+
path = candidate
|
|
312
|
+
i += 1
|
|
313
|
+
continue
|
|
314
|
+
|
|
315
|
+
if line.startswith("@@"):
|
|
316
|
+
in_hunk = bool(_HUNK_HEADER_RE.match(line))
|
|
317
|
+
|
|
318
|
+
i += 1
|
|
319
|
+
|
|
320
|
+
flush(len(lines))
|
|
321
|
+
return sections
|
src/fix/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""The ``gitpr fix`` feature: turn a review finding into a reviewable patch.
|
|
2
|
+
|
|
3
|
+
Modules, in dependency order:
|
|
4
|
+
- patch_provenance.py — the data contract (safety classes, candidates, results)
|
|
5
|
+
- patch_extractor.py — reading the code an AI hands back (shared with the chat)
|
|
6
|
+
- patch_safety_classifier.py — SAFE / REVIEW_REQUIRED / EXPERIMENTAL, pure logic
|
|
7
|
+
- patch_applier.py — the project's first shared git wrapper (apply, check, reverse)
|
|
8
|
+
- fix_history.py — the applied-patch record that makes --rollback deterministic
|
|
9
|
+
- apply_fix.py — the use case: review -> AI findings -> classify -> dry-run/apply
|
|
10
|
+
- rollback_fix.py — the use case that undoes an applied patch
|
|
11
|
+
|
|
12
|
+
Import the submodule you need (``from src.fix.apply_fix import apply_fix``); this
|
|
13
|
+
package deliberately re-exports nothing, because the chat imports
|
|
14
|
+
``src.fix.patch_extractor`` on every session and must not drag the AI and git
|
|
15
|
+
layers in with it.
|
|
16
|
+
"""
|