gitpr-cli 1.5.0__py3-none-any.whl → 1.7.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.
Files changed (43) hide show
  1. {gitpr_cli-1.5.0.dist-info → gitpr_cli-1.7.0.dist-info}/METADATA +2 -2
  2. {gitpr_cli-1.5.0.dist-info → gitpr_cli-1.7.0.dist-info}/RECORD +43 -40
  3. src/application/use_cases/create_baseline.py +2 -1
  4. src/application/use_cases/run_check.py +2 -1
  5. src/ascii_chart.py +248 -0
  6. src/config.py +1266 -1383
  7. src/config_schema.py +1730 -1357
  8. src/core.py +2769 -2328
  9. src/diff_parser.py +102 -0
  10. src/doc_links.py +53 -28
  11. src/i18n.py +142 -136
  12. src/infrastructure/scm/azure_devops_provider.py +491 -464
  13. src/infrastructure/scm/base.py +1 -0
  14. src/infrastructure/scm/bitbucket_provider.py +443 -402
  15. src/infrastructure/scm/factory.py +15 -3
  16. src/infrastructure/scm/gitlab_provider.py +469 -435
  17. src/issue_autoclose/issue_registry.py +285 -235
  18. src/issue_autoclose/register_created_issue.py +81 -80
  19. src/issue_autoclose/resolve_close_issue.py +236 -226
  20. src/ledger.py +7 -2
  21. src/main.py +6277 -45
  22. src/mcp_server.py +21 -5
  23. src/metrics.py +249 -2
  24. src/project_config.py +484 -426
  25. src/release_engine.py +134 -64
  26. src/review/diff_normalizer.py +84 -84
  27. src/reviewer_suggestion.py +2 -0
  28. src/scm_settings.py +441 -0
  29. src/skill_frontmatter.py +70 -0
  30. src/spinner.py +260 -257
  31. src/suggest_reviewers.py +155 -146
  32. src/tui_issue.py +44 -24
  33. src/ui/chat_app.py +4 -3
  34. src/ui/config_app.py +2332 -2043
  35. src/ui/help_screen.py +46 -46
  36. src/ui/issue_app.py +260 -258
  37. src/ui/linter_app.py +48 -46
  38. src/ui/metrics_app.py +129 -22
  39. src/updater.py +2 -2
  40. {gitpr_cli-1.5.0.dist-info → gitpr_cli-1.7.0.dist-info}/WHEEL +0 -0
  41. {gitpr_cli-1.5.0.dist-info → gitpr_cli-1.7.0.dist-info}/entry_points.txt +0 -0
  42. {gitpr_cli-1.5.0.dist-info → gitpr_cli-1.7.0.dist-info}/licenses/LICENSE +0 -0
  43. {gitpr_cli-1.5.0.dist-info → gitpr_cli-1.7.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.5.0
3
+ Version: 1.7.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? Install the thin-client extension in [`extensions/vscode/`](https://gitpr.natanfiuza.dev.br/docs/editor-extension), powered by `gitpr-mcp`.
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.
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,36 +1,39 @@
1
- gitpr_cli-1.5.0.dist-info/licenses/LICENSE,sha256=UQzLC6mwqb-ZNDkBBAqOxC-SMQcEID0gaM6oFcu6u48,27030
1
+ gitpr_cli-1.7.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
+ src/ascii_chart.py,sha256=WFQclaxm5W6bPBBNVlImyrnrJYCSj5gRgEpldYzI7wk,8279
4
5
  src/blame_engine.py,sha256=Zov1hxrt7Dcmf56ovJKz3QmHIfzDKtSub2iItXeg9-A,17104
5
6
  src/cache.py,sha256=frROc69NyTn3TdJIOPctvuH9z_sh2pdKxL4ekMtyh6k,6572
6
7
  src/changelog_builder.py,sha256=Ea0coS6LCqsdxTWrKW8SZyVsD02HuNNYzCoggI0loi8,9922
7
8
  src/chat_memory.py,sha256=PcohnyspAfIwwwbsp9U2Wk_1AylJMFEMoWqRSJe_w30,8949
8
9
  src/commit_classifier.py,sha256=HQHxPbCfgW_GD1_4mbO2No16jh_Ci5RTSoLbTU4IWYI,6920
9
- src/config.py,sha256=3O6h-A_gVJLxxG9M1L6fHCk-6UeP4tscONepZHInWHM,54130
10
- src/config_schema.py,sha256=hqfiNQgyJQ7l_f4VU315gMEUUG_D8490aXDPwMmFw4I,48087
11
- src/core.py,sha256=w29W2Tyx4DqWfLcZ1svMDyEwLfFpVZbFmFi79QE-mLc,88666
12
- src/diff_parser.py,sha256=ZJxiDMWLglk6QADS37S-oMDhuCvZ5eiKOnCWy6E-Avg,11573
13
- src/doc_links.py,sha256=xc9An9_MyLRYGo_4K5qXRqemG5t_QmWTik8lh8Rlpjo,1185
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
13
+ src/diff_parser.py,sha256=JHeaTJflNOEbR9SYK_Ljr2hctrrABUIb31IFHAbyIDo,15405
14
+ src/doc_links.py,sha256=GdANhE6XbEnVDsbpCf22eUZnqFXor_2SU1O4S-pteIE,2028
14
15
  src/github_api.py,sha256=i1o58oKWWmMCerP-g-hOQHGCwPWd01ooKntTKjpFhIA,3948
15
- src/i18n.py,sha256=PdUmtZlH1zogbiDc3j6xiwtnvUd7XiNbiS-xKjYK4WM,4844
16
+ src/i18n.py,sha256=Xp9c1dNGmTHbo8SyXa-LQuO71nWx3RP-Us7jJ7ARtUY,4804
16
17
  src/issue_engine.py,sha256=RmZsQVg6N1vCcpMdv_5FX-cMGgQ3I0vwMN4parQ4zFg,10974
17
- src/ledger.py,sha256=s10Sm0qcA1b916X7m8d2osDThCmSK0UQnmUrkyj8Eao,43663
18
+ src/ledger.py,sha256=3p5nX9gPGHgANQxBxEywrKo3TNb5KdWDeTI2mslYopo,43787
18
19
  src/linter_engine.py,sha256=hIxht8M09Fnm6NiJZOqjI6dyqnFoHUDVA8Ve4dWZV4s,25214
19
20
  src/linter_wizard.py,sha256=mo8aSTZmVLluCOskWv3nji74WPJh_6c122zwADN9aCc,7634
20
- src/main.py,sha256=W7AWcM4vnaqwXSeg30Sc5_uBTK9-HGWFsUVPwOFiGaY,229742
21
- src/mcp_server.py,sha256=SoTS23WbkmE6VowIdQOcUiFkXKlP5aDrr7MSJX5KD5o,112417
22
- src/metrics.py,sha256=g39FYKx-LAuVyjZLxZCU66iulne8mhTLzPbINk5TFnU,40665
21
+ src/main.py,sha256=zo0PEpxWzrsP8o61AAFDWwoThfWj5YLl_75WmKDT12I,246130
22
+ src/mcp_server.py,sha256=z3yf2eyKPxq4trAaeKrLQNQiN_rjeWYzDgbvAY5ziXc,112996
23
+ src/metrics.py,sha256=Pk8-HkK9dvr8MgBx6QJ_5iTLVgpohPww9C4pQ6wWLzM,48650
23
24
  src/net.py,sha256=CjhgmQnp4OHVmO-3Lm-DLhNfo2RW0M9FnD_HYPKcnNI,2848
24
- src/project_config.py,sha256=9mXC5GKLv3pgQvlc50XCBnMVvTtbWCwIHAi-J48XXyA,15278
25
- src/release_engine.py,sha256=gowcInpeAA581YheeBbRk9dKz3CL-K-zY7kEdEw6DqA,38423
25
+ src/project_config.py,sha256=r-gzjKeFuTYvUzOflQlpbijn6uLXtPEUBzA-1PzUR_k,16749
26
+ src/release_engine.py,sha256=S0kgO-a7RSC07AzvRWRlGo32jx38PAQGIzgvYXOfthk,41274
26
27
  src/reviewer_resolution.py,sha256=bFRdf4GVWIpz4kk7cyF7EYWV43aP7V8sAlNGqKRPd7A,5913
27
- src/reviewer_suggestion.py,sha256=4tFwfltsEvx3s8RnUDKMaFGhdtWPWfgM0wqCTzEzWjE,8759
28
+ src/reviewer_suggestion.py,sha256=YeXJLjJjn1YS69FChWZBF4qHUuIwvv_n-GPiymLN8LQ,8904
29
+ src/scm_settings.py,sha256=3pXgBbxqekvD4LM-I3GgfV4IOa6QJn4x3-DPAThCris,14156
28
30
  src/security.py,sha256=fkWy-r4tdISH0xOyWZyR8ZCFyLuYqu8fn9sKsIeNAso,1351
29
31
  src/security_ruleset.py,sha256=4mqmqtWLHuhuCcjjaoKUM_YFzil4cWZ_ubjlOLgbM_4,4874
30
- src/spinner.py,sha256=8w3_BpIi-EkqmjP2OspALx9lWNyn8XwLw44gCTBeQv8,9221
31
- src/suggest_reviewers.py,sha256=WITj8lZOomwSL703Rw-cFBr59LnQ9t0Pl2kALCh9AA8,5327
32
- src/tui_issue.py,sha256=zNGglq_Xk8qTmoFmoQpIZjgIUz7fX7rvyp1fnBuUbME,10612
33
- src/updater.py,sha256=7UeSsDUbYT4OrvX8UnEP0WmUWYZ58oYBS0huCPZn9mo,9560
32
+ src/skill_frontmatter.py,sha256=K-HsjwSYPHUsDRjIiugBE3koxVdGf38L5d4l8eYVqEo,2167
33
+ src/spinner.py,sha256=JGOO_taTXbrQK0KnNxUQ40PcpS802bYYBosWWm_NG9o,9012
34
+ src/suggest_reviewers.py,sha256=LTOtwDIjaBS_FGDU6bjN6oGy-YD112hIPugYytNKD_A,5821
35
+ src/tui_issue.py,sha256=yaNpj9SSXiWWjMJ7a9FABcNWSffCkA1tjUSnJ1hN-MQ,11477
36
+ src/updater.py,sha256=8OwxL8hhh-hzKyQo5sjckw_e4bNlmlKeegwwH35nrG8,9560
34
37
  src/usage_log.py,sha256=CBCNlUm_xftXsBaGRHvsEjTENvZfE-lW70HGLMkisSg,4508
35
38
  src/version_bump.py,sha256=Dw1LZ_c6VzaO_s9cDeYSevoWtXVr_oP70eS_mkrOm8s,3041
36
39
  src/application/__init__.py,sha256=3YXFcmxJh0f7BwryKXxUrOhGrLnZLsagV-VbBIk0eGk,31
@@ -40,14 +43,14 @@ src/application/use_cases/baseline_gate.py,sha256=-3c5iy7i-HJ7_lsgHGftc8JDYOct0O
40
43
  src/application/use_cases/baseline_summary.py,sha256=BK5LibkNbQsIRmYvqv8AcS_kkC1b_Lt9Q6ZiOMzYCO0,10550
41
44
  src/application/use_cases/calculate_risk.py,sha256=Kgd6YVTrt6DtizXAZQ97XCYd0TBXXytiloauke-L-ko,13513
42
45
  src/application/use_cases/compare_against_baseline.py,sha256=hePa-yidv9OfBe8AZoNhu6FA5MBg5izZyL5K9ZAOWAI,1916
43
- src/application/use_cases/create_baseline.py,sha256=amVigQG-E3yCqzPNmCK9wazSC3Rh4sXJgntqhgg6cUU,26637
46
+ src/application/use_cases/create_baseline.py,sha256=ZJKO_hTmC01CLJ-cnbDVddY3PPOgWobNMYq5U94YwG4,26715
44
47
  src/application/use_cases/generate_mentor_explanation.py,sha256=pPXSLYBJwum6a2P1TYv_B1zwAuM3xYtT8T2LhKO60rY,9707
45
48
  src/application/use_cases/generate_pr_explanation.py,sha256=vlR6Lw7XBnHD7Cloz2gZMrW-kx31cBeC2kSM0qSnUjk,3418
46
49
  src/application/use_cases/generate_test_file.py,sha256=U6EyPbZSXEv9A34PYHkQDToenjqu5Fu1h-nIBqcT9Ds,9657
47
50
  src/application/use_cases/install_policy_pack.py,sha256=7s8wUL_SticgRnUMpzLJCmHso6y6tDHLibSdBLyXb0M,2816
48
51
  src/application/use_cases/publish_pull_request.py,sha256=sBQTBhFGJ98YKTqatZdEccTHfNrp3rxZ_qbKUR9nZrw,9762
49
52
  src/application/use_cases/resolve_effective_policy.py,sha256=HQruehhgqzT5cX2YcVftm1cZqiwDS5cAkRO0txj3hns,7771
50
- src/application/use_cases/run_check.py,sha256=fWzMj91Kp5SXgmCwdWS5IjALXX0wNa3-M7yoHWID27g,15835
53
+ src/application/use_cases/run_check.py,sha256=40txVJrLPmRjAJLMVnlYGVm8qo68Cnx5awmVFnnW8fM,15921
51
54
  src/application/use_cases/suppress_finding.py,sha256=a9oXVFZmvNFrKbEMNXepAu9dEIUqqBb-H1ZSjM0hcAs,18292
52
55
  src/application/use_cases/validate_policy_pack.py,sha256=OaHfs_coNFR9s6c2I7MKFVZRy2Hw2q_y9_B48poOZrE,9906
53
56
  src/branding/__init__.py,sha256=BJ7o5Z13LvWnUVb1aN1tz2XlFk7h89MQGkB2nJuJO0s,81
@@ -122,18 +125,18 @@ src/infrastructure/policy/__init__.py,sha256=mgznDOqCM9iazgY8DTi3uJqrc9wqIQp9D5e
122
125
  src/infrastructure/policy/local_policy_repository.py,sha256=-dW4QR-rzUM6qwOSC5UhA51tg1iWAXkTxKcZJPK0eBo,5786
123
126
  src/infrastructure/policy/policy_pack_loader.py,sha256=uRTp64ksVkdC3GBYgaHxkXUjeTk7UWPy0ePWVRo92k4,9559
124
127
  src/infrastructure/scm/__init__.py,sha256=CbW2p9DsYpwwUqy4xdtgoPugsLijopfNwLT-IAfEcqA,1887
125
- src/infrastructure/scm/azure_devops_provider.py,sha256=5k3w6HWpZm2hlv_00E5sQYV7OgZcQh1XwJsNlyPa51k,18384
126
- src/infrastructure/scm/base.py,sha256=SJlrjktVUJapTLxWmtT652e2yvuK0kuMOTNCI5_Wleo,17172
127
- src/infrastructure/scm/bitbucket_provider.py,sha256=gGZo6YSUt2HkVdJdKVizkEF9bv4X0bhrzKGST_iybdQ,15553
128
- src/infrastructure/scm/factory.py,sha256=U-1IkVaWxBCb1-8dbyGBJKCFwQWSqLE2P6ZG4_cad1c,3953
128
+ src/infrastructure/scm/azure_devops_provider.py,sha256=9IpgB-Vqzgj0VDFXaYs4PFRiecsQGU9svW5m3OpefsI,18790
129
+ src/infrastructure/scm/base.py,sha256=9tN2IivEngHoo1pDK3PivWHvHSkXokuyQQUJ680gsXU,17198
130
+ src/infrastructure/scm/bitbucket_provider.py,sha256=A58dMDa60lLUynENrinWENFz5dSA_lb9VJSg22ptbkI,16895
131
+ src/infrastructure/scm/factory.py,sha256=O3bRT6f15AozcldZfiH2LgOQ4g4GqMb0sbEWCIeF0HE,4487
129
132
  src/infrastructure/scm/github_provider.py,sha256=vdJDTTYd-CYkAQos-6IZ2hFOJkXG09SrkIcZzHbuuIc,21316
130
- src/infrastructure/scm/gitlab_provider.py,sha256=8AimDKgVPXkYmvZXUYD-NBWfAk-EjhhSgvVVMiUAoBU,17030
133
+ src/infrastructure/scm/gitlab_provider.py,sha256=VAYjdg5gU43FDamXUNMdFzs9MwIGyNRqMzEnLTacRE8,17951
131
134
  src/infrastructure/scm/web_links.py,sha256=-7oF7oUwK_UvKVJZWDPbTnKBi9JEQu2dMDYtgqH10iM,3724
132
135
  src/issue_autoclose/__init__.py,sha256=9IuLWTrq7Vvk2pf3BWMtxVFJ0ALmmHOpYjG8Rwd_x5U,651
133
136
  src/issue_autoclose/insert_close_line.py,sha256=MY8Hr1HWbdIflI6L0Drg9x3ouEJy9BbYxnm1PNguzxY,1779
134
- src/issue_autoclose/issue_registry.py,sha256=UONwAwVEENh3AA527rs-tme-LBALVnZHbVq8WPTI95c,8731
135
- src/issue_autoclose/register_created_issue.py,sha256=jCSDQWVN6WEhMj8s_Ggkx2FsFQPq2m2ehQom3GI6s2I,3006
136
- src/issue_autoclose/resolve_close_issue.py,sha256=clGc2g1vsmCJ4fxieDsN-zfNyemZ1aYvRStHZlMkFZg,7951
137
+ src/issue_autoclose/issue_registry.py,sha256=ttZ8hjuBRWn3iAK9s-KYBybpsW7NRlVxpLyHzD8eKoY,10297
138
+ src/issue_autoclose/register_created_issue.py,sha256=6RwTYepPu1mvuF2T-RcKL3r9THk9zNwCthjg__uQ9JI,2871
139
+ src/issue_autoclose/resolve_close_issue.py,sha256=IS-F39Kp8yvQJhKozIwy2gFEtCy5m1geCwf4cdcdsWo,8094
137
140
  src/policy_packs/laravel-quality/CHANGELOG.md,sha256=g7f9coAAKmPMa3IxpI1bduvr1WfapgfM6KE_kBs5vLM,1154
138
141
  src/policy_packs/laravel-quality/README.md,sha256=MhSQV-p8F6jmBEenvDvklseQ3FDvnvvmN6A16L6-x4s,1852
139
142
  src/policy_packs/laravel-quality/linter.yml,sha256=bzA9yWf1obx3qDs4TCPzfkinOUJaScDI7mK8BlAm8xo,3191
@@ -148,7 +151,7 @@ src/policy_packs/vue-quality/README.md,sha256=2CCPyZbePU4Aiq06lygcVlOcFtNALJaxh0
148
151
  src/policy_packs/vue-quality/linter.yml,sha256=_G_CMBB4hvo-mkRI0rv77530Es5GnXoNipD5wpfPwvg,2508
149
152
  src/policy_packs/vue-quality/policy.yml,sha256=FyVL17Fl_bgVjxZU5lO7MME5w04vjKV3UfISOBkGBXc,3099
150
153
  src/review/__init__.py,sha256=ux2rkwebfnzuQpFc57eEceOC4CZkojPOtfp951Ux5Gc,700
151
- src/review/diff_normalizer.py,sha256=ITSoJc1s_SKDjSRjFM7pFvotDO9kclW0wf9XSZZcgC4,3635
154
+ src/review/diff_normalizer.py,sha256=8KnFbauGK-fuLGSSL_ox4kAwCE048MuTmorPwBrhlEU,3675
152
155
  src/review/diff_source.py,sha256=V6fMtgxNJbMBL_qRifkLGfzFwtaTCbFFKFvcutHZjLk,2733
153
156
  src/review/remote_pr.py,sha256=9oEEo-v-D08egxwN_EINh5Ju74vow91-CAH1kAb2ql0,13669
154
157
  src/review/render.py,sha256=l63vQ3WiGxPSzCk4JxyQoFNfIyG0AdHgMobAjMeaOcY,3194
@@ -159,13 +162,13 @@ src/split/hunk_grouper.py,sha256=35PU7I_9iUzaN9xaJvoK2y9KGkeKjKofO_7l6xOWWks,145
159
162
  src/split/hunk_parser.py,sha256=DYc82pCKnuquJJwB7nm44WtZKK22lv-d75g9EeBhCm8,10266
160
163
  src/split/split_plan.py,sha256=Y0Ah3LbzixKgAy74dh7XiQImsw6-f2dxwfJSkQHreCw,6572
161
164
  src/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
162
- src/ui/chat_app.py,sha256=y6hDKGgSaJDfp1rZBWxwQVQHzP_xx6-3q-oxxWnTUfg,26839
165
+ src/ui/chat_app.py,sha256=eOhGsaC2EweYNJe9kdCbN9AMYcN_LI2BCj_tmOYZ7Bs,26789
163
166
  src/ui/commit_app.py,sha256=mggIsdHkg3EIvQoSc11XL7tpkSGJ25WWszHdgmJnMjc,8304
164
- src/ui/config_app.py,sha256=YTQSkSWh97UqIWTcwu2_bAbra9C4pt2vgth8F-L-Ad0,84484
165
- src/ui/help_screen.py,sha256=EvbP8kFZ6Sdehi7bJ6Wo_YcTTk5MTkgneeidWwefYrA,1890
166
- src/ui/issue_app.py,sha256=ao2PDiT539Ppc2LItjesmMG7I5Mj96DIy4fFU7UCw_g,10378
167
- src/ui/linter_app.py,sha256=kXhcHlhOMRCKTdfnXlCU34oDvHkm1aRivGWlz0AkeKM,1612
168
- src/ui/metrics_app.py,sha256=fLDVWFVkJSZcDKfx2t5njtjGR_3laVeHu4rm19ByW7E,16759
167
+ src/ui/config_app.py,sha256=thlCw2E8DungQf2W8DoATT2RFioq_vmsyS_IM2nsybo,94671
168
+ src/ui/help_screen.py,sha256=PxdLLey13DycZpIY0nj2Zd0DSF1uV02Gcrq3Ny88CuU,1723
169
+ src/ui/issue_app.py,sha256=ea8RYK0QcS9izoJ9e60TTTznOX8QVhMREW_GQEyi_xM,10192
170
+ src/ui/linter_app.py,sha256=xVMMZJhVQAo6XKI_MrRQvnGXNoDjELRs7k8rNaF4yws,1602
171
+ src/ui/metrics_app.py,sha256=8zbCVwTMN8k6uDHpbXlzBxd1EFIFaDTPDyH4h-0bdXY,20618
169
172
  src/ui/metrics_migration_app.py,sha256=U6l-ORUk1NlC2aIQSLvFjahMVM1dat-wFjWlct0fRYQ,6096
170
173
  src/ui/pr_base_app.py,sha256=OvPcQVeyBrGBH8lhlVy0Ks1kVdvqcYHl7Zc3P2tXPGI,3857
171
174
  src/ui/pr_publish_app.py,sha256=TU5IrSB3bkQEhl2N0W8KCwHfRGGacijqziMD9I7By04,71802
@@ -174,8 +177,8 @@ src/ui/demo/__init__.py,sha256=aOdUN0izuwW2FfLAHG9Qga-DJG5NBVqP0gYNO8OiKls,59
174
177
  src/ui/demo/demo_app.py,sha256=4t_Mct-BJXpe5tlt1XmtRyN1IT0OL6U3WSmGqOJqMG4,3165
175
178
  src/ui/demo/demo_help_screen.py,sha256=I_LELswhPtbHjfMWGVUqLtOwxGbzSYJsVWXjqZrLoLA,2108
176
179
  src/ui/demo/demo_screens.py,sha256=GXaQFTgTn9awSP-EYiBXsCZqB583WrSQcuNYoBK12g8,1997
177
- gitpr_cli-1.5.0.dist-info/METADATA,sha256=xkoqpc842w0Y6RcI0MdeH6VUbn0QvGPKYB821hnIpBE,54016
178
- gitpr_cli-1.5.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
179
- gitpr_cli-1.5.0.dist-info/entry_points.txt,sha256=TstGnMUzdOI5qNmpM97EPDBPuMqOZxoo6EoLxHSOMSQ,71
180
- gitpr_cli-1.5.0.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
181
- gitpr_cli-1.5.0.dist-info/RECORD,,
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,,
@@ -107,7 +107,8 @@ def current_diff(base: str | None = None, quiet: bool = True) -> str:
107
107
  encoding="utf-8",
108
108
  errors="replace",
109
109
  )
110
- return result.stdout if result.returncode == 0 else ""
110
+ from src.diff_parser import collapse_binary_hunks
111
+ return collapse_binary_hunks(result.stdout) if result.returncode == 0 else ""
111
112
 
112
113
 
113
114
  def diff_files(diff_text: str) -> list[str]:
@@ -108,7 +108,8 @@ def get_check_diff(
108
108
  cwd=repo_path or None,
109
109
  )
110
110
  if proc.returncode == 0:
111
- return proc.stdout or ""
111
+ from src.diff_parser import collapse_binary_hunks
112
+ return collapse_binary_hunks(proc.stdout or "")
112
113
  except OSError:
113
114
  pass
114
115
  return ""
src/ascii_chart.py ADDED
@@ -0,0 +1,248 @@
1
+ """
2
+ ASCII / Unicode 2D Chart Rendering Engine for GitPR Metrics.
3
+
4
+ Provides pure functions to render 2D terminal charts (vertical histograms,
5
+ time-series charts) without external dependencies or UI framework coupling.
6
+ """
7
+
8
+ import math
9
+ from typing import Any, Dict, List, Optional
10
+
11
+
12
+ def _fmt_tokens(val: float) -> str:
13
+ """Format token count in human-readable compact form (e.g. 1.2M, 45.0k, 500)."""
14
+ if val >= 1_000_000:
15
+ return f"{val / 1_000_000:.1f}M"
16
+ if val >= 1_000:
17
+ return f"{val / 1_000:.1f}k"
18
+ return str(int(round(val)))
19
+
20
+
21
+ def _duration_ms_label(ms: Optional[float]) -> str:
22
+ """Format duration in milliseconds into ms, s, or min."""
23
+ if ms is None:
24
+ return "0 ms"
25
+ if ms < 1000:
26
+ return f"{ms:.0f} ms"
27
+ seconds = ms / 1000.0
28
+ if seconds < 60:
29
+ return f"{seconds:.1f} s"
30
+ return f"{seconds / 60.0:.1f} min"
31
+
32
+
33
+ def render_empty_card(title: str, message: str, width: int = 60) -> str:
34
+ """Render a framed empty-state banner with informative text."""
35
+ w = max(40, width)
36
+ inner_w = w - 4
37
+ top_border = f"┌{'─' * (w - 2)}┐"
38
+ bottom_border = f"└{'─' * (w - 2)}┘"
39
+ empty_line = f"│ {' ' * inner_w} │"
40
+
41
+ title_line = f"│ [bold cyan]{title:<{inner_w}}[/bold cyan] │"
42
+ msg_line = f"│ [yellow]{message:<{inner_w}}[/yellow] │"
43
+
44
+ return "\n".join([top_border, title_line, empty_line, msg_line, bottom_border])
45
+
46
+
47
+ def render_vertical_histogram(
48
+ latency: Optional[Dict[str, Any]],
49
+ height: int = 7,
50
+ col_width: int = 7,
51
+ ) -> str:
52
+ """Render a 2D vertical ASCII histogram for latency distributions.
53
+
54
+ Args:
55
+ latency: Dict from latency_report() containing 'count', 'average_ms',
56
+ 'median_ms', 'p95_ms', and 'buckets'.
57
+ height: Height in lines for the plotting area.
58
+ col_width: Character width of each column bar.
59
+
60
+ Returns:
61
+ Formatted multi-line string containing the 2D chart.
62
+ """
63
+ if not latency or not latency.get("count") or not latency.get("buckets"):
64
+ return render_empty_card(
65
+ "📊 Latency Distribution",
66
+ "No timed executions recorded in the selected window.",
67
+ )
68
+
69
+ buckets: List[Dict[str, Any]] = latency["buckets"]
70
+ counts = [b["count"] for b in buckets]
71
+ max_count = max(counts) if counts else 0
72
+ if max_count <= 0:
73
+ max_count = 1
74
+
75
+ count_total = latency.get("count", 0)
76
+ avg_lbl = _duration_ms_label(latency.get("average_ms"))
77
+ med_lbl = _duration_ms_label(latency.get("median_ms"))
78
+ p95_lbl = _duration_ms_label(latency.get("p95_ms"))
79
+
80
+ lines: List[str] = []
81
+
82
+ # Title & Summary header
83
+ lines.append("[bold cyan]📊 Latency Histogram[/bold cyan]")
84
+ lines.append(
85
+ f" Timed runs: [bold]{count_total}[/bold] │ "
86
+ f"Average: [cyan]{avg_lbl}[/cyan] │ "
87
+ f"Median (P50): [green]{med_lbl}[/green] │ "
88
+ f"P95: [yellow]{p95_lbl}[/yellow]"
89
+ )
90
+ lines.append("")
91
+
92
+ # Y-axis label formatting width
93
+ y_label_width = max(len(str(max_count)), 3)
94
+
95
+ # Plot rows from top (height) down to 1
96
+ for r in range(height, 0, -1):
97
+ # Y-axis tick mark and label
98
+ ratio = r / height
99
+ level_val = round(ratio * max_count)
100
+ if r == height:
101
+ y_tick = f"{max_count:>{y_label_width}} ┤"
102
+ elif r == math.ceil(height / 2):
103
+ half_val = round(max_count / 2)
104
+ y_tick = f"{half_val:>{y_label_width}} ┤"
105
+ else:
106
+ y_tick = f"{' ' * y_label_width} │"
107
+
108
+ # Build column cells
109
+ cells = []
110
+ for b_count in counts:
111
+ # Thresholds for full block and half block
112
+ val_ratio = b_count / max_count
113
+ if val_ratio >= (r / height):
114
+ # Full block
115
+ bar_chars = "█" * col_width
116
+ elif val_ratio >= ((r - 0.5) / height):
117
+ # Half block
118
+ bar_chars = "▄" * col_width
119
+ else:
120
+ bar_chars = " " * col_width
121
+
122
+ cells.append(bar_chars)
123
+
124
+ row_content = " ".join(cells)
125
+ lines.append(f" {y_tick} {row_content}")
126
+
127
+ # X-axis Baseline
128
+ base_col = "─" * col_width
129
+ x_axis_line = "───".join([base_col] * len(buckets))
130
+ lines.append(f" {0:>{y_label_width}} ┴─{x_axis_line}─")
131
+
132
+ # Bucket Labels
133
+ label_cells = []
134
+ for b in buckets:
135
+ lbl = b.get("label", "")
136
+ label_cells.append(f"{lbl:^{col_width}}")
137
+ lines.append(f" {' ' * y_label_width} " + " ".join(label_cells))
138
+
139
+ # Counts and percentages below labels
140
+ stat_cells = []
141
+ for b in buckets:
142
+ c = b.get("count", 0)
143
+ rate = b.get("rate", 0.0)
144
+ txt = f"{c} ({rate:.0%})"
145
+ stat_cells.append(f"{txt:^{col_width}}")
146
+ lines.append(f" {' ' * y_label_width} " + " ".join(stat_cells))
147
+
148
+ return "\n".join(lines)
149
+
150
+
151
+ def render_token_timeseries(
152
+ trend: Optional[Dict[str, Any]],
153
+ height: int = 7,
154
+ col_width: int = 5,
155
+ ) -> str:
156
+ """Render a 2D continuous ASCII time-series chart for daily token trends.
157
+
158
+ Args:
159
+ trend: Dict from token_trend_report() containing 'days', 'total_tokens',
160
+ 'peak_tokens', 'average_tokens', and 'sparkline'.
161
+ height: Height in lines for the plotting area.
162
+ col_width: Character width allocated per day column.
163
+
164
+ Returns:
165
+ Formatted multi-line string containing the 2D chart.
166
+ """
167
+ if not trend or not trend.get("days") or not trend.get("total_tokens"):
168
+ return render_empty_card(
169
+ "📈 Token Trend & Consumption",
170
+ "No token consumption recorded in the selected window.",
171
+ )
172
+
173
+ days: List[Dict[str, Any]] = trend["days"]
174
+ peak_tokens = trend.get("peak_tokens", 0)
175
+ total_tokens = trend.get("total_tokens", 0)
176
+ avg_tokens = trend.get("average_tokens", 0.0)
177
+ sparkline = trend.get("sparkline", "")
178
+
179
+ if peak_tokens <= 0:
180
+ peak_tokens = 1
181
+
182
+ lines: List[str] = []
183
+
184
+ # Title & Summary header
185
+ lines.append("[bold cyan]📈 Token Consumption Trend[/bold cyan]")
186
+ lines.append(
187
+ f" Total: [bold]{total_tokens:,}[/bold] │ "
188
+ f"Peak: [yellow]{peak_tokens:,}[/yellow] │ "
189
+ f"Daily Avg: [green]{avg_tokens:,.0f}[/green] │ "
190
+ f"Sparkline: {sparkline}"
191
+ )
192
+ lines.append("")
193
+
194
+ y_label_top = _fmt_tokens(peak_tokens)
195
+ y_label_mid = _fmt_tokens(peak_tokens / 2)
196
+ y_label_width = max(len(y_label_top), len(y_label_mid), 4)
197
+
198
+ # Plot matrix from top down
199
+ for r in range(height, 0, -1):
200
+ if r == height:
201
+ y_tick = f"{y_label_top:>{y_label_width}} ┼"
202
+ grid_char = "┈"
203
+ elif r == math.ceil(height / 2):
204
+ y_tick = f"{y_label_mid:>{y_label_width}} ┼"
205
+ grid_char = "┈"
206
+ else:
207
+ y_tick = f"{' ' * y_label_width} │"
208
+ grid_char = " "
209
+
210
+ day_cells = []
211
+ for d in days:
212
+ tokens = d.get("tokens", 0)
213
+ ratio = tokens / peak_tokens
214
+ if ratio >= (r / height):
215
+ cell_str = "█" * col_width
216
+ elif ratio >= ((r - 0.5) / height):
217
+ cell_str = "▄" * col_width
218
+ elif grid_char != " ":
219
+ cell_str = grid_char * col_width
220
+ else:
221
+ cell_str = " " * col_width
222
+ day_cells.append(cell_str)
223
+
224
+ lines.append(f" {y_tick} " + " ".join(day_cells))
225
+
226
+ # X-axis Baseline
227
+ base_col = "─" * col_width
228
+ x_axis_line = "─".join([base_col] * len(days))
229
+ lines.append(f" {'0':>{y_label_width}} ┴─{x_axis_line}─")
230
+
231
+ # Date labels (MM-DD)
232
+ date_cells = []
233
+ for d in days:
234
+ raw_day = d.get("day", "")
235
+ # Format YYYY-MM-DD -> MM-DD
236
+ short_date = raw_day[5:] if len(raw_day) >= 10 else raw_day
237
+ date_cells.append(f"{short_date:^{col_width}}")
238
+ lines.append(f" {' ' * y_label_width} " + " ".join(date_cells))
239
+
240
+ # Compact token labels below dates
241
+ tok_cells = []
242
+ for d in days:
243
+ t = d.get("tokens", 0)
244
+ tok_str = _fmt_tokens(t) if t > 0 else "·"
245
+ tok_cells.append(f"{tok_str:^{col_width}}")
246
+ lines.append(f" {' ' * y_label_width} " + " ".join(tok_cells))
247
+
248
+ return "\n".join(lines)