gitpr-cli 1.4.0__py3-none-any.whl → 1.4.1__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.4.0.dist-info → gitpr_cli-1.4.1.dist-info}/METADATA +7 -1
- {gitpr_cli-1.4.0.dist-info → gitpr_cli-1.4.1.dist-info}/RECORD +21 -11
- src/application/use_cases/publish_pull_request.py +270 -0
- src/application/use_cases/run_check.py +446 -0
- src/core.py +10 -0
- src/domain/check/__init__.py +31 -0
- src/domain/check/check_config.py +122 -0
- src/domain/check/check_output.py +112 -0
- src/domain/check/check_types.py +165 -0
- src/infrastructure/ci/__init__.py +11 -0
- src/infrastructure/ci/junit_builder.py +102 -0
- src/infrastructure/ci/masking.py +65 -0
- src/infrastructure/ci/sarif_builder.py +152 -0
- src/main.py +154 -27
- src/mcp_server.py +312 -5
- src/ui/pr_publish_app.py +58 -8
- src/updater.py +2 -2
- {gitpr_cli-1.4.0.dist-info → gitpr_cli-1.4.1.dist-info}/WHEEL +0 -0
- {gitpr_cli-1.4.0.dist-info → gitpr_cli-1.4.1.dist-info}/entry_points.txt +0 -0
- {gitpr_cli-1.4.0.dist-info → gitpr_cli-1.4.1.dist-info}/licenses/LICENSE +0 -0
- {gitpr_cli-1.4.0.dist-info → gitpr_cli-1.4.1.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.4.
|
|
3
|
+
Version: 1.4.1
|
|
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
|
|
@@ -170,6 +170,7 @@ You can pass the following *flags* for specific actions:
|
|
|
170
170
|
* `gitpr risk`: **Local Risk Scoring for Diffs.** Computes a deterministic, explainable risk score (0.0 to 100.0) and badge (`LOW 🟢`, `MEDIUM 🟡`, `HIGH 🟠`, `CRITICAL 🔴`) using local repository signals (critical paths, missing tests, migrations, bug history, and static findings). Use `--file <path>`, `--format json`, `--base <ref>`, or `--no-history`. Automatically included in code reviews (`GITPR_RISK_INCLUDE_IN_REVIEW=true`). 📖 [Full docs](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/risk-scoring.md)
|
|
171
171
|
* `gitpr policy`: **Shared quality policy for the repository.** Adopts a versioned YAML **Policy Pack** — review skills, linter rules, severity overrides, critical paths, risk weights and PR/commit conventions — and records the choice in `.gitpr/policy.lock.yml`, so the review, the linter and the risk score all run under the same policy, with a checksum per pack. `list`, `validate`, `show`, `use`, `init`, `install`, `off`. 📖 [Full docs](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/policy-packs.md)
|
|
172
172
|
* `gitpr baseline`: **Baseline and auditable suppressions for legacy adoption.** Records the findings a repository already has in `.gitpr/baseline.json`, so a run classifies every finding as `new`, `existing`, `resolved`, `ignored` or `accepted_debt` and **only `new` blocks**. Every suppression carries a reason, and accepted debt carries an owner and an optional deadline. `create`, `show`, `validate`, `update`, `suppress`, `unsuppress`. 📖 [Full docs](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/baseline-suppressions.md)
|
|
173
|
+
* `gitpr check`: **Unified CI check command.** Consolidates static linter, secret scanning, external bridges (Semgrep, Gitleaks, Bandit), optional AI code review, risk scoring, and baseline into a single CI pipeline command. Emits machine-readable artifacts in **SARIF 2.1.0**, **JSON**, or **JUnit XML** with deterministic exit codes (`0`, `1`, `2`). 📖 [Full docs](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/check-ci.md)
|
|
173
174
|
* `-c` or `--commit`: Runs a local `git diff` and displays **only the suggested commit message**.
|
|
174
175
|
* `-r` or `--review`: Performs a detailed **Code Review** of local changes. Use with `--mentor` to append pedagogical mentoring explanations for each finding.
|
|
175
176
|
* `-f` or `--fullreview`: Performs a **Full Code Review** analyzing all changes since the remote branch. Use with `--mentor` to append pedagogical mentoring explanations.
|
|
@@ -458,6 +459,8 @@ Once configured, use natural language in your editor's AI chat:
|
|
|
458
459
|
| `analyze_unstaged_diff` | Unstaged diff only (working tree vs index) |
|
|
459
460
|
| `list_fix_candidates` | Fix candidates of the last review: patch, classification, id (read-only) |
|
|
460
461
|
| `review_remote_pr` | AI review of a pull request already open on the forge, fetched by number (read-only) |
|
|
462
|
+
| `run_check` | Structured CI/Editor quality gate returning normalized findings and pass/fail status |
|
|
463
|
+
| `publish_pull_request` | Publish or update a Pull Request on the remote forge from title and body |
|
|
461
464
|
|
|
462
465
|
### Direct CLI Invocation
|
|
463
466
|
|
|
@@ -478,6 +481,8 @@ This is useful for scripting, CI/CD pipelines, and one-off queries where you don
|
|
|
478
481
|
|
|
479
482
|
📖 **Full documentation:** [docs/mcp-integration.md](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/mcp-integration.md) — available in 5 languages (EN, PT-BR, PT-PT, ES, FR).
|
|
480
483
|
|
|
484
|
+
> 🧩 **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://github.com/gitpr-cli/gitpr.git/blob/main/docs/editor-extension.md), powered by `gitpr-mcp`.
|
|
485
|
+
|
|
481
486
|
> 💬 **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://github.com/gitpr-cli/gitpr.git/blob/main/docs/mcp-prompts.md) for the full list.
|
|
482
487
|
|
|
483
488
|
## 🎯 Smart Excludes (Token Optimization)
|
|
@@ -570,6 +575,7 @@ If you want to implement GitPR as an automated quality barrier in your team, che
|
|
|
570
575
|
|
|
571
576
|
### Configuration & Infrastructure
|
|
572
577
|
|
|
578
|
+
* [**Editor Extension (VS Code & Cursor)**](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/editor-extension.md) — Thin-client VSIX extension driving `gitpr-mcp` over stdio for SCM commit generation, Markdown code reviews, two-step PR publishing, and inline `Problems` diagnostics.
|
|
573
579
|
* [**Interactive Configuration TUI (gitpr config)**](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/config-tui.md) — Master-detail terminal interface to view, edit, and validate settings in `~/.gitpr/.env`.
|
|
574
580
|
* [**Multi-Forge SCM Integration**](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/scm-multiforge.md) — Unified SCM provider layer for GitHub, GitLab, Bitbucket Cloud, and Azure DevOps with `--init` wizard.
|
|
575
581
|
* [**Suggested Reviewers**](https://github.com/gitpr-cli/gitpr.git/blob/main/docs/suggested-reviewers.md) — Automatic reviewer suggestion via Git blame of added lines mapped to forge logins.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
gitpr_cli-1.4.
|
|
1
|
+
gitpr_cli-1.4.1.dist-info/licenses/LICENSE,sha256=UQzLC6mwqb-ZNDkBBAqOxC-SMQcEID0gaM6oFcu6u48,27030
|
|
2
2
|
src/__init__.py,sha256=WZW9x8P5CYnru29UiarFa8q1nQzuwYWyIodSOlruCWI,29
|
|
3
3
|
src/ai_providers.py,sha256=QyFEOsXyA59lZziO5bl-4nlYFJpxBcxU9aLhcTBnkWE,14633
|
|
4
4
|
src/blame_engine.py,sha256=Zov1hxrt7Dcmf56ovJKz3QmHIfzDKtSub2iItXeg9-A,17104
|
|
@@ -8,7 +8,7 @@ src/chat_memory.py,sha256=PcohnyspAfIwwwbsp9U2Wk_1AylJMFEMoWqRSJe_w30,8949
|
|
|
8
8
|
src/commit_classifier.py,sha256=pgZRj8JvkCBgwgzDFNjjNKJKzddVB4HoPBcFM1F0zjE,6742
|
|
9
9
|
src/config.py,sha256=5bs6jxxg-CwmtGfJB2eoMUbwRddOnc_MCcadOOcMbHg,53637
|
|
10
10
|
src/config_schema.py,sha256=z-JC5rULAVkaQG4ppmVcdeNKLGpuXFqglzvCBgZjHVo,43308
|
|
11
|
-
src/core.py,sha256=
|
|
11
|
+
src/core.py,sha256=IO7HM3LaKNfs0rKWnDafAQLxrM3O7nwEWTJ6JJttyPM,85522
|
|
12
12
|
src/diff_parser.py,sha256=62X69ves4tlzNgVKXNsfWdOMwqn2h16RkUOPqszMU7k,11252
|
|
13
13
|
src/doc_links.py,sha256=DuovU-RF9kjbisBdEoGAsuavQ2y0MYDhqy6Sz6vbrGI,1157
|
|
14
14
|
src/github_api.py,sha256=ILm7e03UP80swc3XuJvIJJvhLp81-V_3kQQ1EuAcN8Q,3836
|
|
@@ -17,8 +17,8 @@ src/issue_engine.py,sha256=XwW9C5uX84eOWWZMerm0DvE8LpLRcjhrLsXtt7yftak,10872
|
|
|
17
17
|
src/ledger.py,sha256=s10Sm0qcA1b916X7m8d2osDThCmSK0UQnmUrkyj8Eao,43663
|
|
18
18
|
src/linter_engine.py,sha256=hIxht8M09Fnm6NiJZOqjI6dyqnFoHUDVA8Ve4dWZV4s,25214
|
|
19
19
|
src/linter_wizard.py,sha256=7J_nQFghVwCaKG6QI3K45KcOaxHSH1oTCAvP_6tnCQM,7424
|
|
20
|
-
src/main.py,sha256=
|
|
21
|
-
src/mcp_server.py,sha256=
|
|
20
|
+
src/main.py,sha256=wekm-Q0IBFU6c-rQXoz_56Yxygs-jmNrxHXusbMkBgw,214065
|
|
21
|
+
src/mcp_server.py,sha256=gi4LmMYVN4CZWHQgb-5aV5UD2O-tRYzc_BqNg7t-EFA,111654
|
|
22
22
|
src/metrics.py,sha256=M4Yqa__giuhmkElT-XOWrTfHGsjK_URVLhIZvErD2f0,39560
|
|
23
23
|
src/net.py,sha256=f0MOkXyIxi4HVetsybtWpV_yHjiC17T2JMWySBdoV2I,2774
|
|
24
24
|
src/release_engine.py,sha256=xgsoZUYnFohxemwzRSPymoLvgYsK0Y4HRRtZaItkZEg,37433
|
|
@@ -29,7 +29,7 @@ src/security_ruleset.py,sha256=L754S4RlVoZGWm0nuCoeli9-49mKydZpwi31V319DQ0,4337
|
|
|
29
29
|
src/spinner.py,sha256=63pEotiDa3TVwBr-sn0MJoDPQ_uWJTTwINXieJIDDKc,8964
|
|
30
30
|
src/suggest_reviewers.py,sha256=-wRe0HYksx4FWQZ-eDvU9Ghdk5gqtEt457KQP7sE31Y,5181
|
|
31
31
|
src/tui_issue.py,sha256=WEFxT4kzWQ_KbSDsOnS_iuYXasZySB-68jryGNm0peg,10331
|
|
32
|
-
src/updater.py,sha256=
|
|
32
|
+
src/updater.py,sha256=nRI22O8pIku2xu2eWxZOR-eyswPw5DtV4youbyTwtpA,4771
|
|
33
33
|
src/usage_log.py,sha256=FdfSmZKL_I5BcG750rD4gl3pNHx0jqV8Shij5XkAnzc,4381
|
|
34
34
|
src/version_bump.py,sha256=OjqJe9OL1-1dT5od7-SgU5ZN_dTtR_aKX_oSjSY64YQ,2954
|
|
35
35
|
src/application/__init__.py,sha256=0GADzpCB3CdCzw-d3az0DVOtTvXfCppt-0hY-Js5a1U,29
|
|
@@ -44,7 +44,9 @@ src/application/use_cases/generate_mentor_explanation.py,sha256=KNj5cDm-i7aPJxxb
|
|
|
44
44
|
src/application/use_cases/generate_pr_explanation.py,sha256=5BYUPgGBdR29P-9zt75IcKO4KOwGhZnapws7Y6A4574,3202
|
|
45
45
|
src/application/use_cases/generate_test_file.py,sha256=WlHnhwCWHe-XBrL9m1xvJOkn1wqr8vnsE3s3x9wgUi0,8615
|
|
46
46
|
src/application/use_cases/install_policy_pack.py,sha256=U1XvkHt0elXvyM2VRc6TPNukhUomSI2BSWj09hmLC_8,2745
|
|
47
|
+
src/application/use_cases/publish_pull_request.py,sha256=DPK8ERbO2fsjoWUhcLQs10z1WhHVhsfTvtGumn50uIA,9369
|
|
47
48
|
src/application/use_cases/resolve_effective_policy.py,sha256=nTbjRR4di1GnCRV7l3yDNw27CQV3heAd45sftHxNyNc,7563
|
|
49
|
+
src/application/use_cases/run_check.py,sha256=NEhuOZmdWgPFrOPWNku6MwS1Lp0OArk3OMd78M8_EcE,15246
|
|
48
50
|
src/application/use_cases/suppress_finding.py,sha256=juj2jjk2BLjrAcx6WTKJNr5VMTJQ4Gb3GaEjDNGjM64,17809
|
|
49
51
|
src/application/use_cases/validate_policy_pack.py,sha256=g8jt8HYUcGae69tzfT84A22usbvF9JB3T2ZoG5oyw0A,9643
|
|
50
52
|
src/branding/__init__.py,sha256=6ORCsJHtd422jUiQNMCf6eL8kwW4QMf6dJ9Z4Gf9bEc,80
|
|
@@ -62,6 +64,10 @@ src/domain/baseline/baseline_fingerprint.py,sha256=06i-rxVcElr0md4eveKycl7etsHaE
|
|
|
62
64
|
src/domain/baseline/baseline_manifest.py,sha256=jYy_AHBNP6adFr8FNfZtqQmdZteGNzTlToXHg-BP3RI,10429
|
|
63
65
|
src/domain/baseline/baseline_types.py,sha256=_su74m5HuBu0Z5jxm_elva_CX7EP_6025lbX5EjH2o8,11220
|
|
64
66
|
src/domain/baseline/suppression_policy.py,sha256=GNZ0WN7ww4HcUq82vtiqd8Ere7EuP51VuSCoPzEkLOU,22489
|
|
67
|
+
src/domain/check/__init__.py,sha256=Ngd91XHlsZVj7NByY5DzLhI75W9ZqvpAOmt6GQ2crM8,681
|
|
68
|
+
src/domain/check/check_config.py,sha256=C2RB2CHkpRhEpb8QI7MEuSmwD5LskVNSVj2doz6b818,3679
|
|
69
|
+
src/domain/check/check_output.py,sha256=5W7q0S8ojeoXBDbhgdfTLk8bOYvZmO2rbMMNNzyMi8A,3834
|
|
70
|
+
src/domain/check/check_types.py,sha256=k3UvkjWNsFt-7PTHQzE1F-6SXRni72XHHyXS761g1t4,5056
|
|
65
71
|
src/domain/finding/__init__.py,sha256=Y9n2tmT8zybkv3zkBAoI_2k8C4ode2CWHm6l28BhH9w,166
|
|
66
72
|
src/domain/finding/finding_types.py,sha256=Nocd7SmXMAk3q0gVM6yeh2AW6rmJS6CxOCOIQsmsF-Y,833
|
|
67
73
|
src/domain/linter/__init__.py,sha256=WoLci3LhRc7S2IlytrveRPbEQVrAwI4bqazVmGP08Ak,236
|
|
@@ -96,6 +102,10 @@ src/fix/rollback_fix.py,sha256=rMBYewuvlhJOFdo-Ke_SPfu1O-yd6NJzEYiO5o-LcKk,2501
|
|
|
96
102
|
src/infrastructure/__init__.py,sha256=zTIcY3cscsdkXogSGXoJbH0mVGFbKYTRK32fg26wr9U,81
|
|
97
103
|
src/infrastructure/baseline/__init__.py,sha256=VCnAq7MgyvIAwjd2jznSEvyY42Xg5tmnz4VWBxFA5YI,614
|
|
98
104
|
src/infrastructure/baseline/local_baseline_repository.py,sha256=lE9f2D-6XBPRNi5e32jUA6aG6TUxgAN12ihz-tH3KaM,13460
|
|
105
|
+
src/infrastructure/ci/__init__.py,sha256=weQNs0JnGo0nAB_NKvDzcUvOWx_pQQyWucd63Q0ShLA,334
|
|
106
|
+
src/infrastructure/ci/junit_builder.py,sha256=u3DE_nUq0yntvNHRufFUu35aIkgLKfvj-Tz2a2lWBXk,3819
|
|
107
|
+
src/infrastructure/ci/masking.py,sha256=uzH9uanMzye-kKH0IOfKqdEFUAPcCev17BhGSPzqBoc,2153
|
|
108
|
+
src/infrastructure/ci/sarif_builder.py,sha256=XLkmX5j_dmOWy3ycT98WbP35tuYjDgRwaxenhN9jiIE,5233
|
|
99
109
|
src/infrastructure/git/__init__.py,sha256=j9zQUXKNp5NcOzTK-qHGNh75YkufGlw5rgH1wgRknX4,87
|
|
100
110
|
src/infrastructure/git/identity.py,sha256=fClwWTkGIypquE19dUjwwDQCifdsGQmmRMgVobuBIa8,5722
|
|
101
111
|
src/infrastructure/git/patch_applier.py,sha256=RgbrpAp-6x401Eydz6yLMTOXKn5E9LQzV9ZB_n7E90M,7988
|
|
@@ -150,14 +160,14 @@ src/ui/issue_app.py,sha256=eINMmamloeopbtwyS1rCPwosaHeZW5oLebnEV4TfamY,9281
|
|
|
150
160
|
src/ui/linter_app.py,sha256=Y5vwU7D5PQIR_HaY6t40jZy7Guc8z2w-auyosCQqoro,1566
|
|
151
161
|
src/ui/metrics_app.py,sha256=r5JSS70Fkir0qQ4Q03T6c1R2-WGrvYMNTlHMhjAR2Tc,16303
|
|
152
162
|
src/ui/metrics_migration_app.py,sha256=-56HhWZhtVpGCO53NhUnOpZI9bDesfwy2_w7f31cMsU,5923
|
|
153
|
-
src/ui/pr_publish_app.py,sha256=
|
|
163
|
+
src/ui/pr_publish_app.py,sha256=KAtq0aKtSx7d_J4mTaOBAjIK0z_ftJRsNGGbIo0ZhL8,69540
|
|
154
164
|
src/ui/pr_publish_help.py,sha256=fBiUtD97ngxbSfAvPVGGXU19r5kbALCKyi4QeKqyQ2c,1891
|
|
155
165
|
src/ui/demo/__init__.py,sha256=PGXenIwpS1lVDE164CZ55q8_65XtJq5H300NIpnqOh0,58
|
|
156
166
|
src/ui/demo/demo_app.py,sha256=EDeuqxh_v9KrPhib49TyirmFlHZN7I5NQkg9AjDGhcs,3074
|
|
157
167
|
src/ui/demo/demo_help_screen.py,sha256=m8SuvhzGmkMsahUBA6Aq7ull4W65nS0KOX4hLx-GGps,2056
|
|
158
168
|
src/ui/demo/demo_screens.py,sha256=3Ntp-A7OB43yEtniRqV1UKWc-p5lHRyxeadpe8bxJpM,1937
|
|
159
|
-
gitpr_cli-1.4.
|
|
160
|
-
gitpr_cli-1.4.
|
|
161
|
-
gitpr_cli-1.4.
|
|
162
|
-
gitpr_cli-1.4.
|
|
163
|
-
gitpr_cli-1.4.
|
|
169
|
+
gitpr_cli-1.4.1.dist-info/METADATA,sha256=sNb--Fg6TdLtXclPH_pTKhQvVNAmG4irqz8wDlhATUs,52571
|
|
170
|
+
gitpr_cli-1.4.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
171
|
+
gitpr_cli-1.4.1.dist-info/entry_points.txt,sha256=TstGnMUzdOI5qNmpM97EPDBPuMqOZxoo6EoLxHSOMSQ,71
|
|
172
|
+
gitpr_cli-1.4.1.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
|
|
173
|
+
gitpr_cli-1.4.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""Use case: Publish a pull request directly to the configured SCM forge.
|
|
2
|
+
|
|
3
|
+
Shared by the CLI ``--no-edit`` flow and the MCP ``publish_pull_request`` tool.
|
|
4
|
+
Never commits or pushes; checks for existing PRs or unpushed branches when
|
|
5
|
+
requested, maps forge errors into structured outcomes, and applies defense-in-depth
|
|
6
|
+
secret masking on error messages.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
import subprocess
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
import src.domain.check.check_types # noqa: F401 - avoid circular import through src.infrastructure.ci
|
|
14
|
+
from src.i18n import __
|
|
15
|
+
from src.infrastructure.ci.masking import mask_sensitive_text
|
|
16
|
+
from src.infrastructure.scm import (
|
|
17
|
+
PullRequestRequest,
|
|
18
|
+
PullRequestResult,
|
|
19
|
+
RepoRef,
|
|
20
|
+
ScmProvider,
|
|
21
|
+
ScmProviderError,
|
|
22
|
+
provider_display_name,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class PublishPrOutcome:
|
|
28
|
+
"""Structured result of a pull request publication attempt.
|
|
29
|
+
|
|
30
|
+
status is one of:
|
|
31
|
+
- "success" : PR created on the forge (url and number populated)
|
|
32
|
+
- "already_exists" : an open PR already exists for source_branch (url and number populated)
|
|
33
|
+
- "needs_push" : source_branch is not on origin or has unpushed commits
|
|
34
|
+
- "auth_required" : missing or invalid/expired SCM token (HTTP 401/403)
|
|
35
|
+
- "error" : any other validation or API error
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
status: str
|
|
39
|
+
url: str = ""
|
|
40
|
+
number: int = 0
|
|
41
|
+
provider: str = ""
|
|
42
|
+
source_branch: str = ""
|
|
43
|
+
target_branch: str = ""
|
|
44
|
+
http_status: int = 0
|
|
45
|
+
message: str = ""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def format_recommended_commit_body(commit_message: str, pr_body: str) -> str:
|
|
49
|
+
"""Formats the full PR body with the recommended commit message header (used by --no-edit)."""
|
|
50
|
+
return (
|
|
51
|
+
__("**Recommended Commit Message:**\n")
|
|
52
|
+
+ "```text\n"
|
|
53
|
+
+ f"{commit_message}\n"
|
|
54
|
+
+ "```\n\n---\n\n"
|
|
55
|
+
+ pr_body
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _branch_needs_push(source_branch: str, repo_path: Optional[str] = None) -> bool:
|
|
60
|
+
"""True when origin/<source_branch> does not exist or local HEAD is ahead of it."""
|
|
61
|
+
if not source_branch or source_branch == "HEAD":
|
|
62
|
+
return False
|
|
63
|
+
remote_ref = f"refs/remotes/origin/{source_branch}"
|
|
64
|
+
try:
|
|
65
|
+
proc = subprocess.run(
|
|
66
|
+
["git", "rev-parse", "--verify", remote_ref],
|
|
67
|
+
capture_output=True,
|
|
68
|
+
stdin=subprocess.DEVNULL,
|
|
69
|
+
text=True,
|
|
70
|
+
encoding="utf-8",
|
|
71
|
+
errors="replace",
|
|
72
|
+
cwd=repo_path or None,
|
|
73
|
+
)
|
|
74
|
+
if proc.returncode != 0:
|
|
75
|
+
return True
|
|
76
|
+
ahead = subprocess.run(
|
|
77
|
+
["git", "rev-list", "--count", f"origin/{source_branch}..HEAD"],
|
|
78
|
+
capture_output=True,
|
|
79
|
+
stdin=subprocess.DEVNULL,
|
|
80
|
+
text=True,
|
|
81
|
+
encoding="utf-8",
|
|
82
|
+
errors="replace",
|
|
83
|
+
cwd=repo_path or None,
|
|
84
|
+
)
|
|
85
|
+
if ahead.returncode == 0:
|
|
86
|
+
count = int((ahead.stdout or "0").strip() or "0")
|
|
87
|
+
return count > 0
|
|
88
|
+
except (OSError, ValueError):
|
|
89
|
+
return False
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def publish_pull_request_use_case(
|
|
94
|
+
provider: ScmProvider,
|
|
95
|
+
repo_ref: RepoRef,
|
|
96
|
+
title: str,
|
|
97
|
+
description: str,
|
|
98
|
+
source_branch: str,
|
|
99
|
+
target_branch: str,
|
|
100
|
+
draft: bool = False,
|
|
101
|
+
check_remote_state: bool = False,
|
|
102
|
+
repo_path: Optional[str] = None,
|
|
103
|
+
) -> PublishPrOutcome:
|
|
104
|
+
"""Creates a pull request on *provider* and returns a structured PublishPrOutcome.
|
|
105
|
+
|
|
106
|
+
When *check_remote_state* is True (MCP flow), verifies that a token is present,
|
|
107
|
+
checks whether the branch needs to be pushed first, and checks whether an open
|
|
108
|
+
PR already exists for *source_branch* before attempting creation.
|
|
109
|
+
"""
|
|
110
|
+
label = provider_display_name(provider)
|
|
111
|
+
clean_title = (title or "").strip()
|
|
112
|
+
clean_body = (description or "").strip()
|
|
113
|
+
|
|
114
|
+
if not clean_title:
|
|
115
|
+
return PublishPrOutcome(
|
|
116
|
+
status="error",
|
|
117
|
+
provider=provider.name,
|
|
118
|
+
source_branch=source_branch,
|
|
119
|
+
target_branch=target_branch,
|
|
120
|
+
message=__("Pull request title must not be empty."),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if not clean_body:
|
|
124
|
+
return PublishPrOutcome(
|
|
125
|
+
status="error",
|
|
126
|
+
provider=provider.name,
|
|
127
|
+
source_branch=source_branch,
|
|
128
|
+
target_branch=target_branch,
|
|
129
|
+
message=__("PR description must not be empty."),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if not getattr(provider, "token", ""):
|
|
133
|
+
return PublishPrOutcome(
|
|
134
|
+
status="auth_required",
|
|
135
|
+
provider=provider.name,
|
|
136
|
+
source_branch=source_branch,
|
|
137
|
+
target_branch=target_branch,
|
|
138
|
+
http_status=401,
|
|
139
|
+
message=__(
|
|
140
|
+
"{provider} token is not configured. Run 'gitpr' in a terminal to authenticate.",
|
|
141
|
+
provider=label,
|
|
142
|
+
),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if check_remote_state:
|
|
146
|
+
try:
|
|
147
|
+
existing: Optional[PullRequestResult] = provider.check_existing_pull_request(
|
|
148
|
+
repo_ref, source_branch
|
|
149
|
+
)
|
|
150
|
+
except ScmProviderError as exc:
|
|
151
|
+
if exc.http_status in (401, 403):
|
|
152
|
+
return PublishPrOutcome(
|
|
153
|
+
status="auth_required",
|
|
154
|
+
provider=provider.name,
|
|
155
|
+
source_branch=source_branch,
|
|
156
|
+
target_branch=target_branch,
|
|
157
|
+
http_status=exc.http_status,
|
|
158
|
+
message=mask_sensitive_text(
|
|
159
|
+
__(
|
|
160
|
+
"{provider} token expired or invalid. Run 'gitpr' in a terminal to re-authenticate.",
|
|
161
|
+
provider=label,
|
|
162
|
+
)
|
|
163
|
+
),
|
|
164
|
+
)
|
|
165
|
+
existing = None
|
|
166
|
+
except Exception:
|
|
167
|
+
existing = None
|
|
168
|
+
|
|
169
|
+
if existing is not None:
|
|
170
|
+
return PublishPrOutcome(
|
|
171
|
+
status="already_exists",
|
|
172
|
+
url=existing.url,
|
|
173
|
+
number=existing.number,
|
|
174
|
+
provider=provider.name,
|
|
175
|
+
source_branch=source_branch,
|
|
176
|
+
target_branch=existing.target_branch or target_branch,
|
|
177
|
+
message=__(
|
|
178
|
+
"An open Pull Request already exists for branch '{branch}': {url}",
|
|
179
|
+
branch=source_branch,
|
|
180
|
+
url=existing.url,
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
if _branch_needs_push(source_branch, repo_path=repo_path):
|
|
185
|
+
return PublishPrOutcome(
|
|
186
|
+
status="needs_push",
|
|
187
|
+
provider=provider.name,
|
|
188
|
+
source_branch=source_branch,
|
|
189
|
+
target_branch=target_branch,
|
|
190
|
+
message=__(
|
|
191
|
+
"Branch '{branch}' has not been pushed to origin or has unpushed commits.",
|
|
192
|
+
branch=source_branch,
|
|
193
|
+
),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
result = provider.create_pull_request(
|
|
198
|
+
repo_ref,
|
|
199
|
+
PullRequestRequest(
|
|
200
|
+
title=clean_title,
|
|
201
|
+
description=clean_body,
|
|
202
|
+
source_branch=source_branch,
|
|
203
|
+
target_branch=target_branch,
|
|
204
|
+
draft=bool(draft),
|
|
205
|
+
),
|
|
206
|
+
)
|
|
207
|
+
except ScmProviderError as exc:
|
|
208
|
+
masked_msg = mask_sensitive_text(exc.message)
|
|
209
|
+
if exc.http_status == 401:
|
|
210
|
+
return PublishPrOutcome(
|
|
211
|
+
status="auth_required",
|
|
212
|
+
provider=provider.name,
|
|
213
|
+
source_branch=source_branch,
|
|
214
|
+
target_branch=target_branch,
|
|
215
|
+
http_status=exc.http_status,
|
|
216
|
+
message=masked_msg,
|
|
217
|
+
)
|
|
218
|
+
lower_msg = masked_msg.lower()
|
|
219
|
+
if exc.http_status in (400, 409, 422) and "already exists" in lower_msg:
|
|
220
|
+
return PublishPrOutcome(
|
|
221
|
+
status="already_exists",
|
|
222
|
+
provider=provider.name,
|
|
223
|
+
source_branch=source_branch,
|
|
224
|
+
target_branch=target_branch,
|
|
225
|
+
http_status=exc.http_status,
|
|
226
|
+
message=masked_msg,
|
|
227
|
+
)
|
|
228
|
+
if exc.http_status in (400, 404, 422) and (
|
|
229
|
+
"head" in lower_msg or "source" in lower_msg or "branch" in lower_msg
|
|
230
|
+
) and _branch_needs_push(source_branch, repo_path=repo_path):
|
|
231
|
+
return PublishPrOutcome(
|
|
232
|
+
status="needs_push",
|
|
233
|
+
provider=provider.name,
|
|
234
|
+
source_branch=source_branch,
|
|
235
|
+
target_branch=target_branch,
|
|
236
|
+
http_status=exc.http_status,
|
|
237
|
+
message=masked_msg,
|
|
238
|
+
)
|
|
239
|
+
return PublishPrOutcome(
|
|
240
|
+
status="error",
|
|
241
|
+
provider=provider.name,
|
|
242
|
+
source_branch=source_branch,
|
|
243
|
+
target_branch=target_branch,
|
|
244
|
+
http_status=exc.http_status,
|
|
245
|
+
message=masked_msg,
|
|
246
|
+
)
|
|
247
|
+
except Exception as exc:
|
|
248
|
+
return PublishPrOutcome(
|
|
249
|
+
status="error",
|
|
250
|
+
provider=provider.name,
|
|
251
|
+
source_branch=source_branch,
|
|
252
|
+
target_branch=target_branch,
|
|
253
|
+
http_status=0,
|
|
254
|
+
message=mask_sensitive_text(str(exc)),
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
return PublishPrOutcome(
|
|
258
|
+
status="success",
|
|
259
|
+
url=result.url,
|
|
260
|
+
number=result.number,
|
|
261
|
+
provider=provider.name,
|
|
262
|
+
source_branch=source_branch,
|
|
263
|
+
target_branch=target_branch,
|
|
264
|
+
http_status=201,
|
|
265
|
+
message=__(
|
|
266
|
+
"PR successfully created on {provider}: {pr_url}",
|
|
267
|
+
provider=label,
|
|
268
|
+
pr_url=result.url,
|
|
269
|
+
),
|
|
270
|
+
)
|