pullwise 0.3.0__tar.gz → 0.4.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.
- {pullwise-0.3.0 → pullwise-0.4.0}/PKG-INFO +17 -1
- {pullwise-0.3.0 → pullwise-0.4.0}/README.md +16 -0
- pullwise-0.4.0/pr_pilot/__init__.py +1 -0
- {pullwise-0.3.0 → pullwise-0.4.0}/pr_pilot/analyzer.py +158 -0
- {pullwise-0.3.0 → pullwise-0.4.0}/pr_pilot/cli.py +83 -0
- {pullwise-0.3.0 → pullwise-0.4.0}/pr_pilot/templates.py +54 -0
- {pullwise-0.3.0 → pullwise-0.4.0}/pullwise.egg-info/PKG-INFO +17 -1
- {pullwise-0.3.0 → pullwise-0.4.0}/pyproject.toml +1 -1
- {pullwise-0.3.0 → pullwise-0.4.0}/tests/test_analyzer.py +80 -0
- pullwise-0.3.0/pr_pilot/__init__.py +0 -1
- {pullwise-0.3.0 → pullwise-0.4.0}/pr_pilot/github_client.py +0 -0
- {pullwise-0.3.0 → pullwise-0.4.0}/pullwise.egg-info/SOURCES.txt +0 -0
- {pullwise-0.3.0 → pullwise-0.4.0}/pullwise.egg-info/dependency_links.txt +0 -0
- {pullwise-0.3.0 → pullwise-0.4.0}/pullwise.egg-info/entry_points.txt +0 -0
- {pullwise-0.3.0 → pullwise-0.4.0}/pullwise.egg-info/requires.txt +0 -0
- {pullwise-0.3.0 → pullwise-0.4.0}/pullwise.egg-info/top_level.txt +0 -0
- {pullwise-0.3.0 → pullwise-0.4.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pullwise
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: AI-powered PR descriptions, labels, and code review using OpenAI
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/albertusreza/pr-pilot
|
|
@@ -120,6 +120,8 @@ pr-pilot describe --markdown pr_description.md
|
|
|
120
120
|
| `pr-pilot reviewers` | Suggest reviewers based on git blame of changed files |
|
|
121
121
|
| `pr-pilot standup` | Generate a daily standup update from your recent commits |
|
|
122
122
|
| `pr-pilot todos` | Scan for TODO/FIXME comments and create GitHub issues from them |
|
|
123
|
+
| `pr-pilot commit` | Generate a Conventional Commit message from staged changes |
|
|
124
|
+
| `pr-pilot release` | Full release: changelog + git tag + GitHub release in one command |
|
|
123
125
|
|
|
124
126
|
## Usage
|
|
125
127
|
|
|
@@ -140,6 +142,20 @@ pr-pilot changelog
|
|
|
140
142
|
pr-pilot changelog --output CHANGELOG.md
|
|
141
143
|
```
|
|
142
144
|
|
|
145
|
+
```bash
|
|
146
|
+
# Generate a commit message from staged changes
|
|
147
|
+
pr-pilot commit
|
|
148
|
+
|
|
149
|
+
# Run git commit directly with the generated message
|
|
150
|
+
pr-pilot commit --commit
|
|
151
|
+
|
|
152
|
+
# Full release: bump version + write changelog + create GitHub release
|
|
153
|
+
pr-pilot release --repo owner/repo
|
|
154
|
+
|
|
155
|
+
# Preview release without publishing
|
|
156
|
+
pr-pilot release --repo owner/repo --dry-run
|
|
157
|
+
```
|
|
158
|
+
|
|
143
159
|
```bash
|
|
144
160
|
# Suggest reviewers for your current branch
|
|
145
161
|
pr-pilot reviewers --base main
|
|
@@ -100,6 +100,8 @@ pr-pilot describe --markdown pr_description.md
|
|
|
100
100
|
| `pr-pilot reviewers` | Suggest reviewers based on git blame of changed files |
|
|
101
101
|
| `pr-pilot standup` | Generate a daily standup update from your recent commits |
|
|
102
102
|
| `pr-pilot todos` | Scan for TODO/FIXME comments and create GitHub issues from them |
|
|
103
|
+
| `pr-pilot commit` | Generate a Conventional Commit message from staged changes |
|
|
104
|
+
| `pr-pilot release` | Full release: changelog + git tag + GitHub release in one command |
|
|
103
105
|
|
|
104
106
|
## Usage
|
|
105
107
|
|
|
@@ -120,6 +122,20 @@ pr-pilot changelog
|
|
|
120
122
|
pr-pilot changelog --output CHANGELOG.md
|
|
121
123
|
```
|
|
122
124
|
|
|
125
|
+
```bash
|
|
126
|
+
# Generate a commit message from staged changes
|
|
127
|
+
pr-pilot commit
|
|
128
|
+
|
|
129
|
+
# Run git commit directly with the generated message
|
|
130
|
+
pr-pilot commit --commit
|
|
131
|
+
|
|
132
|
+
# Full release: bump version + write changelog + create GitHub release
|
|
133
|
+
pr-pilot release --repo owner/repo
|
|
134
|
+
|
|
135
|
+
# Preview release without publishing
|
|
136
|
+
pr-pilot release --repo owner/repo --dry-run
|
|
137
|
+
```
|
|
138
|
+
|
|
123
139
|
```bash
|
|
124
140
|
# Suggest reviewers for your current branch
|
|
125
141
|
pr-pilot reviewers --base main
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0"
|
|
@@ -11,6 +11,8 @@ from .templates import (
|
|
|
11
11
|
REVIEWER_SYSTEM, REVIEWER_USER, REVIEWER_COMMENT_HEADER, REVIEWER_COMMENT_TEMPLATE,
|
|
12
12
|
STANDUP_SYSTEM, STANDUP_USER,
|
|
13
13
|
ISSUE_SYSTEM, ISSUE_USER,
|
|
14
|
+
COMMIT_SYSTEM, COMMIT_USER,
|
|
15
|
+
RELEASE_NOTES_SYSTEM, RELEASE_NOTES_USER,
|
|
14
16
|
)
|
|
15
17
|
|
|
16
18
|
_MAX_DIFF_CHARS = 24_000 # stay well within context limits
|
|
@@ -390,3 +392,159 @@ def create_issues_from_todos(
|
|
|
390
392
|
labels=data.get("labels", ["technical-debt"]),
|
|
391
393
|
))
|
|
392
394
|
return issues
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
# ── Commit message generator ──────────────────────────────────────────────────
|
|
398
|
+
|
|
399
|
+
@dataclass
|
|
400
|
+
class CommitMessage:
|
|
401
|
+
subject: str
|
|
402
|
+
body: str | None
|
|
403
|
+
breaking: bool
|
|
404
|
+
footer: str | None
|
|
405
|
+
|
|
406
|
+
def format(self) -> str:
|
|
407
|
+
parts = [self.subject]
|
|
408
|
+
if self.body:
|
|
409
|
+
parts.append("")
|
|
410
|
+
parts.append(self.body)
|
|
411
|
+
if self.footer:
|
|
412
|
+
parts.append("")
|
|
413
|
+
parts.append(self.footer)
|
|
414
|
+
return "\n".join(parts)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _get_staged_diff() -> str:
|
|
418
|
+
diff = _git("diff", "--cached")
|
|
419
|
+
if len(diff) > _MAX_DIFF_CHARS:
|
|
420
|
+
diff = diff[:_MAX_DIFF_CHARS] + "\n\n[diff truncated]"
|
|
421
|
+
return diff
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def generate_commit_message(api_key: str, model: str = _MODEL) -> CommitMessage:
|
|
425
|
+
client = OpenAI(api_key=api_key)
|
|
426
|
+
diff = _get_staged_diff()
|
|
427
|
+
if not diff:
|
|
428
|
+
raise ValueError("No staged changes found. Use 'git add' first.")
|
|
429
|
+
resp = client.chat.completions.create(
|
|
430
|
+
model=model,
|
|
431
|
+
max_tokens=512,
|
|
432
|
+
messages=[
|
|
433
|
+
{"role": "system", "content": COMMIT_SYSTEM},
|
|
434
|
+
{"role": "user", "content": COMMIT_USER.format(diff=diff)},
|
|
435
|
+
],
|
|
436
|
+
)
|
|
437
|
+
raw = (resp.choices[0].message.content or "{}").strip()
|
|
438
|
+
if raw.startswith("```"):
|
|
439
|
+
raw = "\n".join(raw.splitlines()[1:])
|
|
440
|
+
if raw.endswith("```"):
|
|
441
|
+
raw = "\n".join(raw.splitlines()[:-1])
|
|
442
|
+
data = json.loads(raw.strip())
|
|
443
|
+
return CommitMessage(
|
|
444
|
+
subject=data.get("subject", "chore: update"),
|
|
445
|
+
body=data.get("body") or None,
|
|
446
|
+
breaking=data.get("breaking", False),
|
|
447
|
+
footer=data.get("footer") or None,
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
# ── Full release workflow ─────────────────────────────────────────────────────
|
|
452
|
+
|
|
453
|
+
@dataclass
|
|
454
|
+
class ReleaseInfo:
|
|
455
|
+
version: str
|
|
456
|
+
tag: str
|
|
457
|
+
name: str
|
|
458
|
+
body: str
|
|
459
|
+
prerelease: bool
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _generate_release_notes(
|
|
463
|
+
api_key: str, version: str, changelog_md: str, model: str = _MODEL
|
|
464
|
+
) -> tuple[str, str]:
|
|
465
|
+
"""Return (release_name, release_body)."""
|
|
466
|
+
client = OpenAI(api_key=api_key)
|
|
467
|
+
resp = client.chat.completions.create(
|
|
468
|
+
model=model,
|
|
469
|
+
max_tokens=1024,
|
|
470
|
+
messages=[
|
|
471
|
+
{"role": "system", "content": RELEASE_NOTES_SYSTEM},
|
|
472
|
+
{"role": "user", "content": RELEASE_NOTES_USER.format(
|
|
473
|
+
version=version, changelog_md=changelog_md
|
|
474
|
+
)},
|
|
475
|
+
],
|
|
476
|
+
)
|
|
477
|
+
raw = (resp.choices[0].message.content or "{}").strip()
|
|
478
|
+
if raw.startswith("```"):
|
|
479
|
+
raw = "\n".join(raw.splitlines()[1:])
|
|
480
|
+
if raw.endswith("```"):
|
|
481
|
+
raw = "\n".join(raw.splitlines()[:-1])
|
|
482
|
+
data = json.loads(raw.strip())
|
|
483
|
+
return data.get("name", f"v{version}"), data.get("body", changelog_md)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def run_release(
|
|
487
|
+
api_key: str,
|
|
488
|
+
repo: str,
|
|
489
|
+
changelog_path: str = "CHANGELOG.md",
|
|
490
|
+
model: str = _MODEL,
|
|
491
|
+
dry_run: bool = False,
|
|
492
|
+
) -> ReleaseInfo:
|
|
493
|
+
"""Full release: generate changelog → bump version → create GitHub release."""
|
|
494
|
+
import datetime
|
|
495
|
+
from .github_client import _api
|
|
496
|
+
|
|
497
|
+
# 1. Generate changelog entry
|
|
498
|
+
entry, new_version = generate_changelog(api_key, model=model)
|
|
499
|
+
today = datetime.date.today().isoformat()
|
|
500
|
+
changelog_md = entry.to_markdown(new_version, today)
|
|
501
|
+
|
|
502
|
+
# 2. Write CHANGELOG.md
|
|
503
|
+
import pathlib
|
|
504
|
+
p = pathlib.Path(changelog_path)
|
|
505
|
+
if p.exists():
|
|
506
|
+
existing = p.read_text()
|
|
507
|
+
if existing.startswith("# "):
|
|
508
|
+
header, rest = existing.split("\n", 1)
|
|
509
|
+
new_content = f"{header}\n\n{changelog_md}\n{rest}"
|
|
510
|
+
else:
|
|
511
|
+
new_content = f"{changelog_md}\n\n{existing}"
|
|
512
|
+
else:
|
|
513
|
+
new_content = f"# Changelog\n\n{changelog_md}\n"
|
|
514
|
+
|
|
515
|
+
if not dry_run:
|
|
516
|
+
p.write_text(new_content)
|
|
517
|
+
|
|
518
|
+
# 3. Generate release notes
|
|
519
|
+
release_name, release_body = _generate_release_notes(
|
|
520
|
+
api_key, new_version, changelog_md, model=model
|
|
521
|
+
)
|
|
522
|
+
tag = f"v{new_version}"
|
|
523
|
+
|
|
524
|
+
if not dry_run:
|
|
525
|
+
# 4. Commit changelog
|
|
526
|
+
import subprocess
|
|
527
|
+
subprocess.run(["git", "add", changelog_path], check=True)
|
|
528
|
+
subprocess.run(
|
|
529
|
+
["git", "commit", "-m", f"chore: release {tag}"],
|
|
530
|
+
check=True
|
|
531
|
+
)
|
|
532
|
+
# 5. Create git tag
|
|
533
|
+
subprocess.run(["git", "tag", tag], check=True)
|
|
534
|
+
subprocess.run(["git", "push"], check=True)
|
|
535
|
+
subprocess.run(["git", "push", "--tags"], check=True)
|
|
536
|
+
# 6. Create GitHub release
|
|
537
|
+
_api("POST", f"/repos/{repo}/releases", {
|
|
538
|
+
"tag_name": tag,
|
|
539
|
+
"name": release_name,
|
|
540
|
+
"body": release_body,
|
|
541
|
+
"prerelease": False,
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
return ReleaseInfo(
|
|
545
|
+
version=new_version,
|
|
546
|
+
tag=tag,
|
|
547
|
+
name=release_name,
|
|
548
|
+
body=release_body,
|
|
549
|
+
prerelease=False,
|
|
550
|
+
)
|
|
@@ -6,6 +6,7 @@ import sys
|
|
|
6
6
|
from .analyzer import (
|
|
7
7
|
describe_pr, suggest_labels, review_pr, review_pr_as_comment,
|
|
8
8
|
generate_changelog, suggest_reviewers, generate_standup, create_issues_from_todos,
|
|
9
|
+
generate_commit_message, run_release,
|
|
9
10
|
)
|
|
10
11
|
|
|
11
12
|
_RESET = "\033[0m"
|
|
@@ -103,6 +104,71 @@ def cmd_changelog(args: argparse.Namespace) -> None:
|
|
|
103
104
|
print()
|
|
104
105
|
|
|
105
106
|
|
|
107
|
+
def cmd_commit(args: argparse.Namespace) -> None:
|
|
108
|
+
print(f"\n {_DIM}Analyzing staged changes...{_RESET}\n")
|
|
109
|
+
msg = generate_commit_message(_key(), model=args.model)
|
|
110
|
+
|
|
111
|
+
print(f"{_BOLD}Commit message:{_RESET}\n")
|
|
112
|
+
print(f" {_GREEN}{msg.subject}{_RESET}")
|
|
113
|
+
if msg.body:
|
|
114
|
+
print()
|
|
115
|
+
for line in msg.body.splitlines():
|
|
116
|
+
print(f" {line}")
|
|
117
|
+
if msg.footer:
|
|
118
|
+
print(f"\n {_YELLOW}{msg.footer}{_RESET}")
|
|
119
|
+
if msg.breaking:
|
|
120
|
+
print(f"\n {_YELLOW}⚠ Breaking change{_RESET}")
|
|
121
|
+
|
|
122
|
+
if args.commit:
|
|
123
|
+
import subprocess
|
|
124
|
+
full = msg.format()
|
|
125
|
+
result = subprocess.run(["git", "commit", "-m", full])
|
|
126
|
+
if result.returncode == 0:
|
|
127
|
+
print(f"\n {_GREEN}✓{_RESET} Committed.")
|
|
128
|
+
else:
|
|
129
|
+
print(f"\n Commit failed — copy the message above and run git commit manually.")
|
|
130
|
+
elif args.copy:
|
|
131
|
+
try:
|
|
132
|
+
import subprocess
|
|
133
|
+
subprocess.run(["pbcopy"], input=msg.format().encode(), check=True)
|
|
134
|
+
print(f"\n {_GREEN}✓{_RESET} Copied to clipboard")
|
|
135
|
+
except Exception:
|
|
136
|
+
print(f"\n {_DIM}(--copy requires macOS pbcopy){_RESET}")
|
|
137
|
+
print()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def cmd_release(args: argparse.Namespace) -> None:
|
|
141
|
+
repo = args.repo or os.environ.get("GITHUB_REPOSITORY", "")
|
|
142
|
+
if not repo:
|
|
143
|
+
print("pr-pilot release: --repo is required (or set GITHUB_REPOSITORY)", file=sys.stderr)
|
|
144
|
+
sys.exit(1)
|
|
145
|
+
|
|
146
|
+
if args.dry_run:
|
|
147
|
+
print(f"\n {_DIM}Dry run — nothing will be committed or published{_RESET}\n")
|
|
148
|
+
else:
|
|
149
|
+
print(f"\n {_DIM}Running full release workflow for {repo}...{_RESET}\n")
|
|
150
|
+
|
|
151
|
+
release = run_release(
|
|
152
|
+
_key(),
|
|
153
|
+
repo=repo,
|
|
154
|
+
changelog_path=args.changelog,
|
|
155
|
+
model=args.model,
|
|
156
|
+
dry_run=args.dry_run,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
print(f" {_BOLD}Version:{_RESET} {_GREEN}{release.tag}{_RESET}")
|
|
160
|
+
print(f" {_BOLD}Name:{_RESET} {release.name}\n")
|
|
161
|
+
print(release.body)
|
|
162
|
+
|
|
163
|
+
if args.dry_run:
|
|
164
|
+
print(f"\n {_YELLOW}Dry run complete — no changes made.{_RESET}")
|
|
165
|
+
else:
|
|
166
|
+
print(f"\n {_GREEN}✓{_RESET} CHANGELOG.md updated")
|
|
167
|
+
print(f" {_GREEN}✓{_RESET} Committed and tagged {release.tag}")
|
|
168
|
+
print(f" {_GREEN}✓{_RESET} GitHub release created")
|
|
169
|
+
print()
|
|
170
|
+
|
|
171
|
+
|
|
106
172
|
def cmd_reviewers(args: argparse.Namespace) -> None:
|
|
107
173
|
from .github_client import upsert_comment
|
|
108
174
|
from .templates import REVIEWER_COMMENT_HEADER
|
|
@@ -246,6 +312,23 @@ def main() -> None:
|
|
|
246
312
|
p_rev.add_argument("--model", default="gpt-4o", help="OpenAI model to use")
|
|
247
313
|
p_rev.set_defaults(func=cmd_review)
|
|
248
314
|
|
|
315
|
+
# --- commit ---
|
|
316
|
+
p_commit = sub.add_parser("commit", help="Generate a conventional commit message from staged changes")
|
|
317
|
+
p_commit.add_argument("--model", default="gpt-4o")
|
|
318
|
+
p_commit.add_argument("--commit", action="store_true", help="Run git commit with the generated message")
|
|
319
|
+
p_commit.add_argument("--copy", action="store_true", help="Copy message to clipboard (macOS)")
|
|
320
|
+
p_commit.set_defaults(func=cmd_commit)
|
|
321
|
+
|
|
322
|
+
# --- release ---
|
|
323
|
+
p_release = sub.add_parser("release", help="Full release: changelog + git tag + GitHub release")
|
|
324
|
+
p_release.add_argument("--repo", default=None, help="GitHub repo slug (owner/repo)")
|
|
325
|
+
p_release.add_argument("--model", default="gpt-4o")
|
|
326
|
+
p_release.add_argument("--changelog", default="CHANGELOG.md", metavar="FILE",
|
|
327
|
+
help="Path to CHANGELOG.md (default: CHANGELOG.md)")
|
|
328
|
+
p_release.add_argument("--dry-run", action="store_true",
|
|
329
|
+
help="Preview release without committing or publishing")
|
|
330
|
+
p_release.set_defaults(func=cmd_release)
|
|
331
|
+
|
|
249
332
|
# --- reviewers ---
|
|
250
333
|
p_rev2 = sub.add_parser("reviewers", help="Suggest reviewers based on git blame of changed files")
|
|
251
334
|
p_rev2.add_argument("--base", default="main")
|
|
@@ -175,3 +175,57 @@ Comment: {comment}
|
|
|
175
175
|
Surrounding code:
|
|
176
176
|
{context}
|
|
177
177
|
"""
|
|
178
|
+
|
|
179
|
+
# ── Commit message generator ──────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
COMMIT_SYSTEM = """\
|
|
182
|
+
You are an expert at writing git commit messages following the Conventional Commits
|
|
183
|
+
specification (https://www.conventionalcommits.org).
|
|
184
|
+
|
|
185
|
+
Given a staged diff, write a commit message.
|
|
186
|
+
|
|
187
|
+
Return a JSON object:
|
|
188
|
+
{
|
|
189
|
+
"subject": "type(scope): short description, max 72 chars, lowercase after colon",
|
|
190
|
+
"body": "optional longer explanation (2-4 sentences). null if the subject is self-explanatory.",
|
|
191
|
+
"breaking": true or false,
|
|
192
|
+
"footer": "BREAKING CHANGE: description" or null
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
Types: feat | fix | docs | style | refactor | perf | test | chore | ci | build
|
|
196
|
+
Rules:
|
|
197
|
+
- subject must be lowercase after the colon
|
|
198
|
+
- subject must NOT end with a period
|
|
199
|
+
- body wraps at 72 chars
|
|
200
|
+
- Return ONLY the JSON object, no markdown fences
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
COMMIT_USER = """\
|
|
204
|
+
Staged diff:
|
|
205
|
+
{diff}
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
# ── Full release workflow ─────────────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
RELEASE_NOTES_SYSTEM = """\
|
|
211
|
+
You are a technical writer creating GitHub release notes for a software project.
|
|
212
|
+
Given a changelog entry and version info, write engaging release notes.
|
|
213
|
+
|
|
214
|
+
Return a JSON object:
|
|
215
|
+
{
|
|
216
|
+
"name": "release name (e.g. 'v1.2.0 — Dark Mode & Performance')",
|
|
217
|
+
"body": "markdown release notes: lead with a 1-2 sentence overview, then bullet sections for Added/Changed/Fixed. Max 400 words.",
|
|
218
|
+
"prerelease": false
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
Rules:
|
|
222
|
+
- Be specific about what changed. No filler phrases.
|
|
223
|
+
- Use emoji sparingly: one per section header max.
|
|
224
|
+
- Return ONLY the JSON object, no markdown fences
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
RELEASE_NOTES_USER = """\
|
|
228
|
+
Version: {version}
|
|
229
|
+
Changelog entry:
|
|
230
|
+
{changelog_md}
|
|
231
|
+
"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pullwise
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: AI-powered PR descriptions, labels, and code review using OpenAI
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/albertusreza/pr-pilot
|
|
@@ -120,6 +120,8 @@ pr-pilot describe --markdown pr_description.md
|
|
|
120
120
|
| `pr-pilot reviewers` | Suggest reviewers based on git blame of changed files |
|
|
121
121
|
| `pr-pilot standup` | Generate a daily standup update from your recent commits |
|
|
122
122
|
| `pr-pilot todos` | Scan for TODO/FIXME comments and create GitHub issues from them |
|
|
123
|
+
| `pr-pilot commit` | Generate a Conventional Commit message from staged changes |
|
|
124
|
+
| `pr-pilot release` | Full release: changelog + git tag + GitHub release in one command |
|
|
123
125
|
|
|
124
126
|
## Usage
|
|
125
127
|
|
|
@@ -140,6 +142,20 @@ pr-pilot changelog
|
|
|
140
142
|
pr-pilot changelog --output CHANGELOG.md
|
|
141
143
|
```
|
|
142
144
|
|
|
145
|
+
```bash
|
|
146
|
+
# Generate a commit message from staged changes
|
|
147
|
+
pr-pilot commit
|
|
148
|
+
|
|
149
|
+
# Run git commit directly with the generated message
|
|
150
|
+
pr-pilot commit --commit
|
|
151
|
+
|
|
152
|
+
# Full release: bump version + write changelog + create GitHub release
|
|
153
|
+
pr-pilot release --repo owner/repo
|
|
154
|
+
|
|
155
|
+
# Preview release without publishing
|
|
156
|
+
pr-pilot release --repo owner/repo --dry-run
|
|
157
|
+
```
|
|
158
|
+
|
|
143
159
|
```bash
|
|
144
160
|
# Suggest reviewers for your current branch
|
|
145
161
|
pr-pilot reviewers --base main
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
import json
|
|
3
|
+
import pytest
|
|
3
4
|
from unittest.mock import MagicMock, patch
|
|
4
5
|
from pr_pilot.analyzer import (
|
|
5
6
|
describe_pr, suggest_labels, review_pr, PRDescription,
|
|
6
7
|
review_pr_as_comment, generate_changelog, ChangelogEntry,
|
|
7
8
|
suggest_reviewers, generate_standup, create_issues_from_todos, _bump_version,
|
|
9
|
+
generate_commit_message, CommitMessage, run_release,
|
|
8
10
|
)
|
|
9
11
|
from pr_pilot.templates import REVIEW_COMMENT_HEADER, REVIEWER_COMMENT_HEADER
|
|
10
12
|
|
|
@@ -238,3 +240,81 @@ def test_create_issues_from_todos(tmp_path):
|
|
|
238
240
|
assert len(issues) == 1
|
|
239
241
|
assert issues[0].title == "Fix slow foo()"
|
|
240
242
|
assert "technical-debt" in issues[0].labels
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# ── Commit message generator tests ───────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
@patch("pr_pilot.analyzer._get_staged_diff", return_value="+ def login(user, password):\n+ return auth(user)")
|
|
248
|
+
def test_generate_commit_message_basic(mock_diff):
|
|
249
|
+
payload = {
|
|
250
|
+
"subject": "feat(auth): add login function",
|
|
251
|
+
"body": "Implements basic login using the auth helper.",
|
|
252
|
+
"breaking": False,
|
|
253
|
+
"footer": None,
|
|
254
|
+
}
|
|
255
|
+
with patch("pr_pilot.analyzer.OpenAI") as MockOpenAI:
|
|
256
|
+
MockOpenAI.return_value.chat.completions.create.return_value = \
|
|
257
|
+
_mock_openai_response(json.dumps(payload))
|
|
258
|
+
msg = generate_commit_message("fake-key")
|
|
259
|
+
assert msg.subject == "feat(auth): add login function"
|
|
260
|
+
assert msg.breaking is False
|
|
261
|
+
assert msg.footer is None
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@patch("pr_pilot.analyzer._get_staged_diff", return_value="")
|
|
265
|
+
def test_generate_commit_message_no_staged(mock_diff):
|
|
266
|
+
with pytest.raises(ValueError, match="No staged changes"):
|
|
267
|
+
generate_commit_message("fake-key")
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def test_commit_message_format_full():
|
|
271
|
+
msg = CommitMessage(
|
|
272
|
+
subject="feat(api): add rate limiting",
|
|
273
|
+
body="Adds sliding window rate limiting to all endpoints.",
|
|
274
|
+
breaking=True,
|
|
275
|
+
footer="BREAKING CHANGE: Rate limit headers are now always present.",
|
|
276
|
+
)
|
|
277
|
+
formatted = msg.format()
|
|
278
|
+
assert "feat(api): add rate limiting" in formatted
|
|
279
|
+
assert "sliding window" in formatted
|
|
280
|
+
assert "BREAKING CHANGE" in formatted
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def test_commit_message_format_subject_only():
|
|
284
|
+
msg = CommitMessage(subject="chore: update deps", body=None, breaking=False, footer=None)
|
|
285
|
+
assert msg.format() == "chore: update deps"
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# ── Release workflow tests ────────────────────────────────────────────────────
|
|
289
|
+
|
|
290
|
+
@patch("pr_pilot.analyzer._get_commits_since_tag", return_value="abc Fix login\ndef Add dark mode")
|
|
291
|
+
@patch("pr_pilot.analyzer._get_current_version", return_value="1.0.0")
|
|
292
|
+
@patch("pr_pilot.analyzer.get_diff", return_value="+ new code")
|
|
293
|
+
@patch("pr_pilot.analyzer.get_commits", return_value="")
|
|
294
|
+
@patch("pr_pilot.analyzer.get_branch", return_value="main")
|
|
295
|
+
def test_run_release_dry_run(mock_branch, mock_commits, mock_diff, mock_ver, mock_ctag, tmp_path):
|
|
296
|
+
changelog_payload = {
|
|
297
|
+
"version": "minor", "highlights": "New features.",
|
|
298
|
+
"added": ["Dark mode"], "changed": [], "fixed": ["Login bug"],
|
|
299
|
+
"removed": [], "security": [],
|
|
300
|
+
}
|
|
301
|
+
release_payload = {
|
|
302
|
+
"name": "v1.1.0 — Dark Mode",
|
|
303
|
+
"body": "## What's new\n- Dark mode added\n- Login bug fixed",
|
|
304
|
+
"prerelease": False,
|
|
305
|
+
}
|
|
306
|
+
responses = [
|
|
307
|
+
_mock_openai_response(json.dumps(changelog_payload)),
|
|
308
|
+
_mock_openai_response(json.dumps(release_payload)),
|
|
309
|
+
]
|
|
310
|
+
changelog_file = str(tmp_path / "CHANGELOG.md")
|
|
311
|
+
with patch("pr_pilot.analyzer.OpenAI") as MockOpenAI:
|
|
312
|
+
MockOpenAI.return_value.chat.completions.create.side_effect = responses
|
|
313
|
+
release = run_release("fake-key", repo="owner/repo",
|
|
314
|
+
changelog_path=changelog_file, dry_run=True)
|
|
315
|
+
assert release.version == "1.1.0"
|
|
316
|
+
assert release.tag == "v1.1.0"
|
|
317
|
+
assert "Dark Mode" in release.name
|
|
318
|
+
# dry run: changelog file should NOT be written
|
|
319
|
+
import pathlib
|
|
320
|
+
assert not pathlib.Path(changelog_file).exists()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.3.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|