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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitpr-cli
3
- Version: 1.4.0
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.0.dist-info/licenses/LICENSE,sha256=UQzLC6mwqb-ZNDkBBAqOxC-SMQcEID0gaM6oFcu6u48,27030
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=rw_S8_QEv6kljpC6X4CEJLPjKtd91SogtH5G9-c2_PA,84823
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=jgRHRpbTYkhCaamnXJacThdXPih2iE3005GVhbqYWW8,210498
21
- src/mcp_server.py,sha256=rLkN8rJAjqSaguCXksbQsLBYu8XKkWdXKps_zl6dwOQ,99398
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=mjlTehzEo4u23dOezGA-iMrw1GzAENUqQuZx7JZJLHs,4771
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=cZ9xr1TSZF6fjvQPlAdSjzSNykufHF1l7hsmo3Ib2j0,67311
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.0.dist-info/METADATA,sha256=UElFetNIOs54YQZ92CqK1LxNKFSAp-0VG-azLmnsgF0,51335
160
- gitpr_cli-1.4.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
161
- gitpr_cli-1.4.0.dist-info/entry_points.txt,sha256=TstGnMUzdOI5qNmpM97EPDBPuMqOZxoo6EoLxHSOMSQ,71
162
- gitpr_cli-1.4.0.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
163
- gitpr_cli-1.4.0.dist-info/RECORD,,
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
+ )