pr-descriptor 0.1.0__tar.gz
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.
- pr_descriptor-0.1.0/PKG-INFO +11 -0
- pr_descriptor-0.1.0/pr_descriptor.egg-info/PKG-INFO +11 -0
- pr_descriptor-0.1.0/pr_descriptor.egg-info/SOURCES.txt +12 -0
- pr_descriptor-0.1.0/pr_descriptor.egg-info/dependency_links.txt +1 -0
- pr_descriptor-0.1.0/pr_descriptor.egg-info/entry_points.txt +2 -0
- pr_descriptor-0.1.0/pr_descriptor.egg-info/requires.txt +6 -0
- pr_descriptor-0.1.0/pr_descriptor.egg-info/top_level.txt +1 -0
- pr_descriptor-0.1.0/pr_writer/__init__.py +0 -0
- pr_descriptor-0.1.0/pr_writer/ai_client.py +53 -0
- pr_descriptor-0.1.0/pr_writer/cli.py +124 -0
- pr_descriptor-0.1.0/pr_writer/git_utils.py +117 -0
- pr_descriptor-0.1.0/pr_writer/platforms.py +114 -0
- pr_descriptor-0.1.0/pyproject.toml +24 -0
- pr_descriptor-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pr-descriptor
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI-powered PR description generator using Claude
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: anthropic>=0.40.0
|
|
7
|
+
Requires-Dist: click>=8.1.0
|
|
8
|
+
Requires-Dist: rich>=13.0.0
|
|
9
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
10
|
+
Requires-Dist: pyperclip>=1.9.0
|
|
11
|
+
Requires-Dist: requests>=2.31.0
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pr-descriptor
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI-powered PR description generator using Claude
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: anthropic>=0.40.0
|
|
7
|
+
Requires-Dist: click>=8.1.0
|
|
8
|
+
Requires-Dist: rich>=13.0.0
|
|
9
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
10
|
+
Requires-Dist: pyperclip>=1.9.0
|
|
11
|
+
Requires-Dist: requests>=2.31.0
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
pr_descriptor.egg-info/PKG-INFO
|
|
3
|
+
pr_descriptor.egg-info/SOURCES.txt
|
|
4
|
+
pr_descriptor.egg-info/dependency_links.txt
|
|
5
|
+
pr_descriptor.egg-info/entry_points.txt
|
|
6
|
+
pr_descriptor.egg-info/requires.txt
|
|
7
|
+
pr_descriptor.egg-info/top_level.txt
|
|
8
|
+
pr_writer/__init__.py
|
|
9
|
+
pr_writer/ai_client.py
|
|
10
|
+
pr_writer/cli.py
|
|
11
|
+
pr_writer/git_utils.py
|
|
12
|
+
pr_writer/platforms.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pr_writer
|
|
File without changes
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from typing import Iterator
|
|
2
|
+
|
|
3
|
+
from anthropic import Anthropic
|
|
4
|
+
|
|
5
|
+
from .git_utils import GitContext
|
|
6
|
+
|
|
7
|
+
SYSTEM_PROMPT = """\
|
|
8
|
+
You are an expert software engineer who writes clear, concise, and professional pull request descriptions.
|
|
9
|
+
|
|
10
|
+
Given git context (branch name, commits, changed files, diff), generate a well-structured PR description \
|
|
11
|
+
in GitHub-flavored Markdown using this exact format:
|
|
12
|
+
|
|
13
|
+
## Summary
|
|
14
|
+
A concise paragraph explaining what this PR does and the motivation behind it.
|
|
15
|
+
|
|
16
|
+
## Changes
|
|
17
|
+
- Bullet points listing the key changes
|
|
18
|
+
|
|
19
|
+
## Testing
|
|
20
|
+
Steps or notes on how to test these changes.
|
|
21
|
+
|
|
22
|
+
## Breaking Changes
|
|
23
|
+
List any breaking changes, or write "None" if there are none.
|
|
24
|
+
|
|
25
|
+
## Related Issues
|
|
26
|
+
Reference issue numbers mentioned in commits (e.g., Fixes #123), or write "None".
|
|
27
|
+
|
|
28
|
+
Be specific and professional. Focus on the *why* as much as the *what*. No filler phrases.\
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _build_prompt(ctx: GitContext) -> str:
|
|
33
|
+
commits = "\n".join(ctx.commits) if ctx.commits else "No commits"
|
|
34
|
+
files = "\n".join(ctx.changed_files) if ctx.changed_files else "No changed files"
|
|
35
|
+
return (
|
|
36
|
+
f"Branch: `{ctx.current_branch}` → `{ctx.base_branch}`\n\n"
|
|
37
|
+
f"Commits:\n{commits}\n\n"
|
|
38
|
+
f"Changed files:\n{files}\n\n"
|
|
39
|
+
f"Diff:\n```diff\n{ctx.diff}\n```"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def stream_pr_description(ctx: GitContext) -> Iterator[str]:
|
|
44
|
+
"""Stream the PR description token by token from Claude."""
|
|
45
|
+
client = Anthropic()
|
|
46
|
+
with client.messages.stream(
|
|
47
|
+
model="claude-sonnet-4-6",
|
|
48
|
+
max_tokens=1024,
|
|
49
|
+
system=SYSTEM_PROMPT,
|
|
50
|
+
messages=[{"role": "user", "content": _build_prompt(ctx)}],
|
|
51
|
+
) as stream:
|
|
52
|
+
for text in stream.text_stream:
|
|
53
|
+
yield text
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from dotenv import load_dotenv
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.markdown import Markdown
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
|
|
11
|
+
from .ai_client import stream_pr_description
|
|
12
|
+
from .git_utils import Platform, collect_git_context
|
|
13
|
+
from .platforms import PlatformError, push_description
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _load_env() -> None:
|
|
19
|
+
load_dotenv()
|
|
20
|
+
load_dotenv(Path.home() / ".pr-writer" / ".env")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@click.command()
|
|
24
|
+
@click.option("--base", "-b", default="main", show_default=True, help="Base branch to compare against.")
|
|
25
|
+
@click.option("--repo", "-r", default=".", show_default=True, help="Path to the git repository.")
|
|
26
|
+
@click.option("--copy", "-c", is_flag=True, help="Copy the generated description to clipboard.")
|
|
27
|
+
@click.option("--push", "-p", is_flag=True, help="Update the open PR on GitHub/Gitea with the description.")
|
|
28
|
+
@click.option("--raw", is_flag=True, help="Print raw Markdown without the formatted preview panel.")
|
|
29
|
+
@click.version_option(version="0.1.0")
|
|
30
|
+
def main(base: str, repo: str, copy: bool, push: bool, raw: bool) -> None:
|
|
31
|
+
"""Generate AI-powered PR descriptions from your git diff.
|
|
32
|
+
|
|
33
|
+
\b
|
|
34
|
+
Reads your local git history, sends the context to Claude, and streams
|
|
35
|
+
back a structured PR description. Supports GitHub and Gitea remotes.
|
|
36
|
+
|
|
37
|
+
\b
|
|
38
|
+
Examples:
|
|
39
|
+
pr-writer # compare current branch vs main
|
|
40
|
+
pr-writer --base develop # compare vs develop
|
|
41
|
+
pr-writer --copy # copy output to clipboard
|
|
42
|
+
pr-writer --push # update the open PR description via API
|
|
43
|
+
"""
|
|
44
|
+
_load_env()
|
|
45
|
+
|
|
46
|
+
if not os.getenv("ANTHROPIC_API_KEY"):
|
|
47
|
+
console.print("[red]Error:[/red] ANTHROPIC_API_KEY is not set.")
|
|
48
|
+
console.print("Add it to a [bold].env[/bold] file in your project or home directory.")
|
|
49
|
+
sys.exit(1)
|
|
50
|
+
|
|
51
|
+
# ── Collect git context ──────────────────────────────────────────────────
|
|
52
|
+
with console.status("[bold blue]Reading git context...", spinner="dots"):
|
|
53
|
+
try:
|
|
54
|
+
ctx = collect_git_context(repo, base)
|
|
55
|
+
except RuntimeError as exc:
|
|
56
|
+
console.print(f"[red]Git error:[/red] {exc}")
|
|
57
|
+
sys.exit(1)
|
|
58
|
+
|
|
59
|
+
if not ctx.commits and not ctx.diff:
|
|
60
|
+
console.print(
|
|
61
|
+
f"[yellow]No changes[/yellow] between "
|
|
62
|
+
f"[bold]{ctx.current_branch}[/bold] and [bold]{base}[/bold]."
|
|
63
|
+
)
|
|
64
|
+
sys.exit(0)
|
|
65
|
+
|
|
66
|
+
# ── Print summary line ───────────────────────────────────────────────────
|
|
67
|
+
remote_label = ""
|
|
68
|
+
if ctx.remote:
|
|
69
|
+
platform_name = "GitHub" if ctx.remote.platform == Platform.GITHUB else "Gitea"
|
|
70
|
+
remote_label = (
|
|
71
|
+
f" [dim]Remote:[/dim] {platform_name} "
|
|
72
|
+
f"([dim]{ctx.remote.owner}/{ctx.remote.repo}[/dim])"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
console.print(
|
|
76
|
+
f"[dim]Branch:[/dim] [bold cyan]{ctx.current_branch}[/bold cyan] → [bold]{base}[/bold]"
|
|
77
|
+
f" [dim]Commits:[/dim] {len(ctx.commits)}"
|
|
78
|
+
f" [dim]Files:[/dim] {len(ctx.changed_files)}"
|
|
79
|
+
+ remote_label
|
|
80
|
+
)
|
|
81
|
+
console.print()
|
|
82
|
+
|
|
83
|
+
# ── Stream description from Claude ───────────────────────────────────────
|
|
84
|
+
console.print("[bold green]Generating PR description[/bold green]\n")
|
|
85
|
+
parts: list[str] = []
|
|
86
|
+
try:
|
|
87
|
+
for token in stream_pr_description(ctx):
|
|
88
|
+
console.print(token, end="")
|
|
89
|
+
parts.append(token)
|
|
90
|
+
except Exception as exc:
|
|
91
|
+
console.print(f"\n[red]API error:[/red] {exc}")
|
|
92
|
+
sys.exit(1)
|
|
93
|
+
|
|
94
|
+
description = "".join(parts)
|
|
95
|
+
console.print("\n")
|
|
96
|
+
|
|
97
|
+
# ── Formatted preview ────────────────────────────────────────────────────
|
|
98
|
+
if not raw:
|
|
99
|
+
console.print(
|
|
100
|
+
Panel(
|
|
101
|
+
Markdown(description),
|
|
102
|
+
title="[bold]Formatted Preview[/bold]",
|
|
103
|
+
border_style="dim",
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# ── Copy to clipboard ────────────────────────────────────────────────────
|
|
108
|
+
if copy:
|
|
109
|
+
try:
|
|
110
|
+
import pyperclip # noqa: PLC0415
|
|
111
|
+
pyperclip.copy(description)
|
|
112
|
+
console.print("[green]Copied to clipboard.[/green]")
|
|
113
|
+
except Exception as exc:
|
|
114
|
+
console.print(f"[yellow]Could not copy to clipboard:[/yellow] {exc}")
|
|
115
|
+
|
|
116
|
+
# ── Push to platform ─────────────────────────────────────────────────────
|
|
117
|
+
if push:
|
|
118
|
+
with console.status("[bold blue]Updating PR description...", spinner="dots"):
|
|
119
|
+
try:
|
|
120
|
+
pr_url = push_description(ctx, description)
|
|
121
|
+
except PlatformError as exc:
|
|
122
|
+
console.print(f"[red]Push failed:[/red] {exc}")
|
|
123
|
+
sys.exit(1)
|
|
124
|
+
console.print(f"[green]PR description updated:[/green] {pr_url}")
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import subprocess
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Platform(Enum):
|
|
10
|
+
GITHUB = "github"
|
|
11
|
+
GITEA = "gitea"
|
|
12
|
+
UNKNOWN = "unknown"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class RemoteInfo:
|
|
17
|
+
platform: Platform
|
|
18
|
+
host: str
|
|
19
|
+
owner: str
|
|
20
|
+
repo: str
|
|
21
|
+
url: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class GitContext:
|
|
26
|
+
current_branch: str
|
|
27
|
+
base_branch: str
|
|
28
|
+
commits: List[str]
|
|
29
|
+
diff: str
|
|
30
|
+
changed_files: List[str]
|
|
31
|
+
repo_path: str
|
|
32
|
+
remote: Optional[RemoteInfo] = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _run(args: List[str], cwd: str) -> str:
|
|
36
|
+
"""Run a git command and return stdout. Raises RuntimeError on failure."""
|
|
37
|
+
result = subprocess.run(
|
|
38
|
+
["git"] + args,
|
|
39
|
+
cwd=cwd,
|
|
40
|
+
capture_output=True,
|
|
41
|
+
text=True,
|
|
42
|
+
encoding="utf-8",
|
|
43
|
+
errors="replace",
|
|
44
|
+
)
|
|
45
|
+
if result.returncode != 0:
|
|
46
|
+
raise RuntimeError(result.stderr.strip() or f"git {' '.join(args)} failed")
|
|
47
|
+
return result.stdout.strip()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def parse_remote_url(url: str) -> Optional[RemoteInfo]:
|
|
51
|
+
"""
|
|
52
|
+
Parse a git remote URL into a RemoteInfo.
|
|
53
|
+
|
|
54
|
+
Handles both HTTPS and SSH formats:
|
|
55
|
+
https://github.com/owner/repo.git
|
|
56
|
+
git@github.com:owner/repo.git
|
|
57
|
+
"""
|
|
58
|
+
https_re = re.compile(r"https?://([^/]+)/([^/]+)/([^/]+?)(?:\.git)?$")
|
|
59
|
+
ssh_re = re.compile(r"git@([^:]+):([^/]+)/([^/]+?)(?:\.git)?$")
|
|
60
|
+
|
|
61
|
+
match = https_re.match(url) or ssh_re.match(url)
|
|
62
|
+
if not match:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
host, owner, repo = match.group(1), match.group(2), match.group(3)
|
|
66
|
+
platform = Platform.GITHUB if host == "github.com" else Platform.GITEA
|
|
67
|
+
|
|
68
|
+
return RemoteInfo(platform=platform, host=host, owner=owner, repo=repo, url=url)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_remote_info(repo_path: str) -> Optional[RemoteInfo]:
|
|
72
|
+
try:
|
|
73
|
+
url = _run(["remote", "get-url", "origin"], repo_path)
|
|
74
|
+
return parse_remote_url(url)
|
|
75
|
+
except RuntimeError:
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_current_branch(repo_path: str) -> str:
|
|
80
|
+
return _run(["branch", "--show-current"], repo_path)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_commits(repo_path: str, base_branch: str) -> List[str]:
|
|
84
|
+
output = _run(
|
|
85
|
+
["log", f"{base_branch}..HEAD", "--oneline", "--no-merges"],
|
|
86
|
+
repo_path,
|
|
87
|
+
)
|
|
88
|
+
return [line for line in output.splitlines() if line]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_diff(repo_path: str, base_branch: str, max_chars: int = 14000) -> str:
|
|
92
|
+
diff = _run(["diff", f"{base_branch}...HEAD", "--no-color"], repo_path)
|
|
93
|
+
if len(diff) > max_chars:
|
|
94
|
+
diff = (
|
|
95
|
+
diff[:max_chars]
|
|
96
|
+
+ f"\n\n... [diff truncated — {len(diff) - max_chars} additional chars omitted]"
|
|
97
|
+
)
|
|
98
|
+
return diff
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_changed_files(repo_path: str, base_branch: str) -> List[str]:
|
|
102
|
+
output = _run(["diff", f"{base_branch}...HEAD", "--name-status"], repo_path)
|
|
103
|
+
return [line for line in output.splitlines() if line]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def collect_git_context(repo_path: str, base_branch: str) -> GitContext:
|
|
107
|
+
"""Gather all git context needed to generate a PR description."""
|
|
108
|
+
resolved = str(Path(repo_path).resolve())
|
|
109
|
+
return GitContext(
|
|
110
|
+
current_branch=get_current_branch(resolved),
|
|
111
|
+
base_branch=base_branch,
|
|
112
|
+
commits=get_commits(resolved, base_branch),
|
|
113
|
+
diff=get_diff(resolved, base_branch),
|
|
114
|
+
changed_files=get_changed_files(resolved, base_branch),
|
|
115
|
+
repo_path=resolved,
|
|
116
|
+
remote=get_remote_info(resolved),
|
|
117
|
+
)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
from .git_utils import GitContext, Platform, RemoteInfo
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PlatformError(Exception):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
# Header helpers
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
def _github_headers(token: str) -> dict:
|
|
18
|
+
return {
|
|
19
|
+
"Authorization": f"Bearer {token}",
|
|
20
|
+
"Accept": "application/vnd.github+json",
|
|
21
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _gitea_headers(token: str) -> dict:
|
|
26
|
+
return {
|
|
27
|
+
"Authorization": f"token {token}",
|
|
28
|
+
"Content-Type": "application/json",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# PR lookup
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
def _find_github_pr(remote: RemoteInfo, branch: str, token: str) -> Optional[int]:
|
|
37
|
+
url = f"https://api.github.com/repos/{remote.owner}/{remote.repo}/pulls"
|
|
38
|
+
resp = requests.get(
|
|
39
|
+
url,
|
|
40
|
+
headers=_github_headers(token),
|
|
41
|
+
params={"state": "open", "head": f"{remote.owner}:{branch}"},
|
|
42
|
+
timeout=10,
|
|
43
|
+
)
|
|
44
|
+
resp.raise_for_status()
|
|
45
|
+
prs = resp.json()
|
|
46
|
+
return prs[0]["number"] if prs else None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _find_gitea_pr(remote: RemoteInfo, branch: str, token: str) -> Optional[int]:
|
|
50
|
+
url = f"https://{remote.host}/api/v1/repos/{remote.owner}/{remote.repo}/pulls"
|
|
51
|
+
resp = requests.get(
|
|
52
|
+
url,
|
|
53
|
+
headers=_gitea_headers(token),
|
|
54
|
+
params={"state": "open", "limit": 50},
|
|
55
|
+
timeout=10,
|
|
56
|
+
)
|
|
57
|
+
resp.raise_for_status()
|
|
58
|
+
for pr in resp.json():
|
|
59
|
+
head = pr.get("head", {})
|
|
60
|
+
if head.get("ref") == branch or head.get("label") == branch:
|
|
61
|
+
return pr["number"]
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
# Public API
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
def push_description(ctx: GitContext, description: str) -> str:
|
|
70
|
+
"""
|
|
71
|
+
Find the open PR for the current branch and update its description.
|
|
72
|
+
Returns the PR URL on success. Raises PlatformError on failure.
|
|
73
|
+
"""
|
|
74
|
+
remote = ctx.remote
|
|
75
|
+
if remote is None:
|
|
76
|
+
raise PlatformError("Could not detect a git remote. Is 'origin' set?")
|
|
77
|
+
|
|
78
|
+
if remote.platform == Platform.GITHUB:
|
|
79
|
+
token = os.getenv("GITHUB_TOKEN")
|
|
80
|
+
if not token:
|
|
81
|
+
raise PlatformError("GITHUB_TOKEN is not set in your environment.")
|
|
82
|
+
|
|
83
|
+
pr_number = _find_github_pr(remote, ctx.current_branch, token)
|
|
84
|
+
if pr_number is None:
|
|
85
|
+
raise PlatformError(
|
|
86
|
+
f"No open PR found for branch '{ctx.current_branch}' "
|
|
87
|
+
f"on {remote.owner}/{remote.repo}."
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
url = f"https://api.github.com/repos/{remote.owner}/{remote.repo}/pulls/{pr_number}"
|
|
91
|
+
resp = requests.patch(
|
|
92
|
+
url, headers=_github_headers(token), json={"body": description}, timeout=10
|
|
93
|
+
)
|
|
94
|
+
resp.raise_for_status()
|
|
95
|
+
return f"https://github.com/{remote.owner}/{remote.repo}/pull/{pr_number}"
|
|
96
|
+
|
|
97
|
+
else: # Gitea
|
|
98
|
+
token = os.getenv("GITEA_TOKEN")
|
|
99
|
+
if not token:
|
|
100
|
+
raise PlatformError("GITEA_TOKEN is not set in your environment.")
|
|
101
|
+
|
|
102
|
+
pr_number = _find_gitea_pr(remote, ctx.current_branch, token)
|
|
103
|
+
if pr_number is None:
|
|
104
|
+
raise PlatformError(
|
|
105
|
+
f"No open PR found for branch '{ctx.current_branch}' "
|
|
106
|
+
f"on {remote.owner}/{remote.repo}."
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
url = f"https://{remote.host}/api/v1/repos/{remote.owner}/{remote.repo}/pulls/{pr_number}"
|
|
110
|
+
resp = requests.patch(
|
|
111
|
+
url, headers=_gitea_headers(token), json={"body": description}, timeout=10
|
|
112
|
+
)
|
|
113
|
+
resp.raise_for_status()
|
|
114
|
+
return f"https://{remote.host}/{remote.owner}/{remote.repo}/pulls/{pr_number}"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pr-descriptor"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "AI-powered PR description generator using Claude"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"anthropic>=0.40.0",
|
|
12
|
+
"click>=8.1.0",
|
|
13
|
+
"rich>=13.0.0",
|
|
14
|
+
"python-dotenv>=1.0.0",
|
|
15
|
+
"pyperclip>=1.9.0",
|
|
16
|
+
"requests>=2.31.0",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
pr-descriptor = "pr_writer.cli:main"
|
|
21
|
+
|
|
22
|
+
[tool.setuptools.packages.find]
|
|
23
|
+
where = ["."]
|
|
24
|
+
include = ["pr_writer*"]
|