usepr 0.1.2__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.
- usepr/__init__.py +0 -0
- usepr/cli/__init__.py +0 -0
- usepr/cli/commands/__init__.py +0 -0
- usepr/cli/commands/generate_command.py +178 -0
- usepr/cli/templates/command.py.j2 +67 -0
- usepr/cli/themes/default.toml +29 -0
- usepr/cli/themes/title.txt +7 -0
- usepr/cli/usecli.config.toml +11 -0
- usepr/configs/__init__.py +0 -0
- usepr/configs/dspy.py +59 -0
- usepr/modules/__init__.py +0 -0
- usepr/modules/pull_request_summary_generator.py +35 -0
- usepr/services/__init__.py +1 -0
- usepr/services/pr_summary_service.py +115 -0
- usepr/signatures/__init__.py +0 -0
- usepr/signatures/pull_request_summary_generator.py +44 -0
- usepr/utils/__init__.py +0 -0
- usepr/utils/git.py +126 -0
- usepr/utils/github.py +85 -0
- usepr-0.1.2.dist-info/METADATA +198 -0
- usepr-0.1.2.dist-info/RECORD +24 -0
- usepr-0.1.2.dist-info/WHEEL +4 -0
- usepr-0.1.2.dist-info/entry_points.txt +3 -0
- usepr-0.1.2.dist-info/licenses/LICENSE +21 -0
usepr/__init__.py
ADDED
|
File without changes
|
usepr/cli/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""GenerateCommand - CLI command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Annotated, Optional
|
|
7
|
+
|
|
8
|
+
import pyperclip
|
|
9
|
+
import typer
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich.syntax import Syntax
|
|
12
|
+
from usecli import BaseCommand, Prompt, console, theme
|
|
13
|
+
|
|
14
|
+
from usepr.configs.dspy import configure_dspy
|
|
15
|
+
from usepr.services.pr_summary_service import (
|
|
16
|
+
gather_commits,
|
|
17
|
+
generate_summary,
|
|
18
|
+
get_templates,
|
|
19
|
+
)
|
|
20
|
+
from usepr.utils.github import PrTemplate
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class GenerateCommand(BaseCommand):
|
|
24
|
+
def signature(self) -> str:
|
|
25
|
+
return "generate"
|
|
26
|
+
|
|
27
|
+
def description(self) -> str:
|
|
28
|
+
return "Description for generate command"
|
|
29
|
+
|
|
30
|
+
def aliases(self) -> list[str]:
|
|
31
|
+
return ["gen"]
|
|
32
|
+
|
|
33
|
+
def _prompt_for_template(self, templates: list[PrTemplate]) -> PrTemplate | None:
|
|
34
|
+
"""Prompt the user to select a PR template, or skip."""
|
|
35
|
+
if not templates:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
if len(templates) == 1:
|
|
39
|
+
t = templates[0]
|
|
40
|
+
label = t.name or os.path.basename(t.path)
|
|
41
|
+
use = (
|
|
42
|
+
(
|
|
43
|
+
Prompt.ask(
|
|
44
|
+
f"\n[bold {theme.ACCENT}]Found PR template:[/bold {theme.ACCENT}] {label} — use it?",
|
|
45
|
+
default="y",
|
|
46
|
+
choices=["y", "n"],
|
|
47
|
+
show_choices=False,
|
|
48
|
+
)
|
|
49
|
+
or ""
|
|
50
|
+
)
|
|
51
|
+
.lower()
|
|
52
|
+
.strip()
|
|
53
|
+
)
|
|
54
|
+
return t if use == "y" else None
|
|
55
|
+
|
|
56
|
+
console.print(
|
|
57
|
+
f"\n[bold {theme.ACCENT}]Found {len(templates)} PR templates:[/bold {theme.ACCENT}]"
|
|
58
|
+
)
|
|
59
|
+
for i, t in enumerate(templates, 1):
|
|
60
|
+
label = t.name or os.path.basename(t.path)
|
|
61
|
+
console.print(f" {i}. {label} [dim]({t.path})[/dim]")
|
|
62
|
+
|
|
63
|
+
console.print(" 0. [dim]Skip template[/dim]")
|
|
64
|
+
|
|
65
|
+
choice = Prompt.ask(
|
|
66
|
+
"Select a template number",
|
|
67
|
+
default="0",
|
|
68
|
+
)
|
|
69
|
+
try:
|
|
70
|
+
idx = int(choice) if choice else 0
|
|
71
|
+
except ValueError:
|
|
72
|
+
return None
|
|
73
|
+
if idx == 0 or idx > len(templates):
|
|
74
|
+
return None
|
|
75
|
+
return templates[idx - 1]
|
|
76
|
+
|
|
77
|
+
def handle(
|
|
78
|
+
self,
|
|
79
|
+
model: Annotated[
|
|
80
|
+
Optional[str],
|
|
81
|
+
typer.Option("-m", "--model", help="Override the default LLM model."),
|
|
82
|
+
] = None,
|
|
83
|
+
) -> None:
|
|
84
|
+
|
|
85
|
+
configure_dspy(model)
|
|
86
|
+
|
|
87
|
+
repo = os.path.abspath(".")
|
|
88
|
+
|
|
89
|
+
console.clear()
|
|
90
|
+
console.print(f"[bold {theme.PRIMARY}]Pull Request Summary Generator")
|
|
91
|
+
|
|
92
|
+
from usepr.utils.git import get_default_branch
|
|
93
|
+
|
|
94
|
+
default_branch = get_default_branch(repo).strip()
|
|
95
|
+
base_branch = (
|
|
96
|
+
Prompt.ask(
|
|
97
|
+
f"\n[bold {theme.ACCENT}]Base branch to diff against[/bold {theme.ACCENT}] [dim](default: {default_branch})[/dim]",
|
|
98
|
+
default="",
|
|
99
|
+
)
|
|
100
|
+
or ""
|
|
101
|
+
).strip()
|
|
102
|
+
|
|
103
|
+
if not base_branch:
|
|
104
|
+
base_branch = default_branch
|
|
105
|
+
|
|
106
|
+
commit_ctx = gather_commits(repo, base_branch)
|
|
107
|
+
|
|
108
|
+
if not commit_ctx.commits:
|
|
109
|
+
console.print(
|
|
110
|
+
"[red]No commits found between the specified tags or branches."
|
|
111
|
+
)
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
# proceed = Prompt.ask(
|
|
115
|
+
# "Do you want to proceed with generating the summary?",
|
|
116
|
+
# default="y",
|
|
117
|
+
# choices=["y", "n"],
|
|
118
|
+
# show_choices=False,
|
|
119
|
+
# )
|
|
120
|
+
# if proceed and proceed.lower() != "y":
|
|
121
|
+
# console.print("Aborting summary generation.")
|
|
122
|
+
# return
|
|
123
|
+
|
|
124
|
+
related_issues = Prompt.ask(
|
|
125
|
+
"List any related issues or tasks (comma-separated), or leave blank if none:",
|
|
126
|
+
default="",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
templates = get_templates(repo)
|
|
130
|
+
selected_template = self._prompt_for_template(templates)
|
|
131
|
+
template_content = selected_template.content if selected_template else None
|
|
132
|
+
|
|
133
|
+
result = generate_summary(
|
|
134
|
+
commits=commit_ctx.commits,
|
|
135
|
+
related_issues=related_issues,
|
|
136
|
+
template=template_content,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
console.print()
|
|
140
|
+
|
|
141
|
+
summary_md = Syntax(
|
|
142
|
+
result.summary,
|
|
143
|
+
"markdown",
|
|
144
|
+
theme="monokai",
|
|
145
|
+
line_numbers=False,
|
|
146
|
+
word_wrap=True,
|
|
147
|
+
)
|
|
148
|
+
console.print(
|
|
149
|
+
Panel(
|
|
150
|
+
summary_md,
|
|
151
|
+
title="Pull Request Summary",
|
|
152
|
+
border_style=theme.SECONDARY,
|
|
153
|
+
title_align="left",
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
copy_to_clipboard = (
|
|
158
|
+
(
|
|
159
|
+
Prompt.ask(
|
|
160
|
+
f"\n[bold {theme.WARNING}]Copy to clipboard?[/bold {theme.WARNING}]",
|
|
161
|
+
default="y",
|
|
162
|
+
choices=["y", "n"],
|
|
163
|
+
)
|
|
164
|
+
or ""
|
|
165
|
+
)
|
|
166
|
+
.lower()
|
|
167
|
+
.strip()
|
|
168
|
+
)
|
|
169
|
+
if copy_to_clipboard in ["y", "yes"]:
|
|
170
|
+
try:
|
|
171
|
+
pyperclip.copy(result.summary)
|
|
172
|
+
console.print(
|
|
173
|
+
f"[bold {theme.SECONDARY}]✓ Summary copied to clipboard![/bold {theme.SECONDARY}]"
|
|
174
|
+
)
|
|
175
|
+
except Exception as e:
|
|
176
|
+
console.print(
|
|
177
|
+
f"[bold {theme.ERROR}]✗ Failed to copy to clipboard: {e}[/bold {theme.ERROR}]"
|
|
178
|
+
)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""{{ class_name }} - CLI command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from usecli import Argument, BaseCommand, Confirm, Menu, Option, Prompt, console, theme
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class {{ class_name }}(BaseCommand):
|
|
9
|
+
def signature(self) -> str:
|
|
10
|
+
return "{{ command_name }}"
|
|
11
|
+
|
|
12
|
+
def description(self) -> str:
|
|
13
|
+
return "Description for {{ command_name }} command"
|
|
14
|
+
|
|
15
|
+
# def aliases(self) -> list[str]:
|
|
16
|
+
# return ["{{ command_name[:3] }}"]
|
|
17
|
+
|
|
18
|
+
def handle(
|
|
19
|
+
self,
|
|
20
|
+
name: str = Argument(..., help="An example argument"),
|
|
21
|
+
verbose: bool = Option(False, "--verbose", "-v", help="Enable verbose output"),
|
|
22
|
+
) -> None:
|
|
23
|
+
console.print(
|
|
24
|
+
f"[bold {theme.SUCCESS}]Executing {{ command_name }}[/bold {theme.SUCCESS}]"
|
|
25
|
+
)
|
|
26
|
+
console.print(f"Hello, {name}!")
|
|
27
|
+
|
|
28
|
+
if verbose:
|
|
29
|
+
console.print("[dim]Verbose mode enabled[/dim]")
|
|
30
|
+
|
|
31
|
+
# Example: Text input prompt
|
|
32
|
+
favorite_color = Prompt.ask(
|
|
33
|
+
"What's your favorite color?",
|
|
34
|
+
choices=["red", "green", "blue", "yellow"],
|
|
35
|
+
)
|
|
36
|
+
console.print(
|
|
37
|
+
f"You chose: [bold {theme.ACCENT}]{favorite_color}[/bold {theme.ACCENT}]"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Example: Confirmation prompt (only when verbose)
|
|
41
|
+
if verbose:
|
|
42
|
+
if not Confirm.ask("Do you want to continue?"):
|
|
43
|
+
console.print(f"[{theme.WARNING}]Cancelled by user[/{theme.WARNING}]")
|
|
44
|
+
return
|
|
45
|
+
console.print(f"[{theme.SUCCESS}]Proceeding...[/{theme.SUCCESS}]")
|
|
46
|
+
|
|
47
|
+
# Example: Single-select menu
|
|
48
|
+
single_choice = Menu.select(
|
|
49
|
+
["Option A", "Option B", "Option C"],
|
|
50
|
+
title="Pick one option:",
|
|
51
|
+
)
|
|
52
|
+
if single_choice:
|
|
53
|
+
console.print(f"You selected: {single_choice}")
|
|
54
|
+
|
|
55
|
+
# Example: Multi-select menu
|
|
56
|
+
multi_choices = Menu.multi_select(
|
|
57
|
+
["Feature 1", "Feature 2", "Feature 3", "Feature 4"],
|
|
58
|
+
title="Select multiple features (space to select, enter to confirm):",
|
|
59
|
+
)
|
|
60
|
+
if multi_choices:
|
|
61
|
+
console.print(f"You selected {len(multi_choices)} features:")
|
|
62
|
+
for choice in multi_choices:
|
|
63
|
+
console.print(f" - {choice}")
|
|
64
|
+
|
|
65
|
+
console.print(
|
|
66
|
+
f"[bold {theme.PRIMARY}]Command completed![/bold {theme.PRIMARY}]"
|
|
67
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[colors]
|
|
2
|
+
# Core semantic colors
|
|
3
|
+
primary = "#60D7FF"
|
|
4
|
+
secondary = "#5EFF87"
|
|
5
|
+
accent = "#F5FE53"
|
|
6
|
+
|
|
7
|
+
success = "#5EFF87" # Green
|
|
8
|
+
error = "#FE686B" # Red
|
|
9
|
+
warning = "#F5FE53" # Yellow
|
|
10
|
+
info = "#60D7FF" # Teal / Blue
|
|
11
|
+
|
|
12
|
+
# Text
|
|
13
|
+
foreground = "#FFFFFF" # Text
|
|
14
|
+
foreground_muted = "#BBBBBB" # Subtext
|
|
15
|
+
|
|
16
|
+
# Surfaces
|
|
17
|
+
background = "#000000" # Base
|
|
18
|
+
border = "#60D7FF" # Surface
|
|
19
|
+
border_focus = "#5EFF87" # Focus Ring
|
|
20
|
+
|
|
21
|
+
# UI semantics
|
|
22
|
+
command = "#60D7FF"
|
|
23
|
+
option = "#60D7FF"
|
|
24
|
+
link = "#60D7FF"
|
|
25
|
+
prompt = "#5EFF87"
|
|
26
|
+
|
|
27
|
+
panel_primary = "#5EFF87"
|
|
28
|
+
panel_secondary = "#60D7FF"
|
|
29
|
+
panel_accent = "#F5FE53"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
[usecli]
|
|
2
|
+
command_name = "usepr"
|
|
3
|
+
title = "usepr"
|
|
4
|
+
title_file = "themes/title.txt"
|
|
5
|
+
title_font = "ansi_regular"
|
|
6
|
+
description = "Generate Pull Request based on git diff"
|
|
7
|
+
commands_dir = "commands"
|
|
8
|
+
templates_dir = "templates"
|
|
9
|
+
themes_dir = "themes"
|
|
10
|
+
theme = "default"
|
|
11
|
+
hide_inspire = true
|
|
File without changes
|
usepr/configs/dspy.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import dspy
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
APPLICATION_NAME = "usepr"
|
|
10
|
+
|
|
11
|
+
DEFAULT_MODEL = "openrouter/openai/gpt-oss-120b"
|
|
12
|
+
|
|
13
|
+
DEFAULT_EXTRA_BODY: dict[str, Any] = {
|
|
14
|
+
"provider": {"order": ["groq"], "allow_fallbacks": False}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
CONFIG_DIR = Path.home() / ".config" / "usepr"
|
|
18
|
+
CONFIG_FILE = CONFIG_DIR / "config.yml"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def load_config() -> dict[str, Any]:
|
|
22
|
+
"""Load configuration from ~/.config/usepr/config.yml."""
|
|
23
|
+
if not CONFIG_FILE.exists():
|
|
24
|
+
return {}
|
|
25
|
+
try:
|
|
26
|
+
with open(CONFIG_FILE) as f:
|
|
27
|
+
data = yaml.safe_load(f)
|
|
28
|
+
return data if isinstance(data, dict) else {}
|
|
29
|
+
except (yaml.YAMLError, OSError):
|
|
30
|
+
return {}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_lm(model: str | None = None) -> dspy.LM:
|
|
34
|
+
"""Get a configured DSPy LM instance.
|
|
35
|
+
|
|
36
|
+
Priority: explicit model arg > config file > default.
|
|
37
|
+
"""
|
|
38
|
+
config = load_config()
|
|
39
|
+
|
|
40
|
+
resolved_model = model or config.get("model") or DEFAULT_MODEL
|
|
41
|
+
|
|
42
|
+
extra_body = config.get("extra_body", DEFAULT_EXTRA_BODY)
|
|
43
|
+
cache = config.get("cache", False)
|
|
44
|
+
|
|
45
|
+
return dspy.LM(
|
|
46
|
+
resolved_model,
|
|
47
|
+
cache=cache,
|
|
48
|
+
extra_body=extra_body,
|
|
49
|
+
extra_headers={
|
|
50
|
+
"HTTP-Referer": f"http://{APPLICATION_NAME}.local",
|
|
51
|
+
"X-Title": APPLICATION_NAME,
|
|
52
|
+
},
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def configure_dspy(model: str | None = None) -> None:
|
|
57
|
+
"""Configure DSPy with the resolved LM."""
|
|
58
|
+
lm = get_lm(model)
|
|
59
|
+
dspy.configure(lm=lm)
|
|
File without changes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import dspy
|
|
2
|
+
|
|
3
|
+
from usepr.signatures.pull_request_summary_generator import (
|
|
4
|
+
RULES,
|
|
5
|
+
TEMPLATE_RULES,
|
|
6
|
+
PullRequestSummaryGeneratorSignature,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PullRequestSummaryGeneratorModule(dspy.Module):
|
|
11
|
+
def __init__(self, callbacks=None):
|
|
12
|
+
super().__init__(callbacks)
|
|
13
|
+
self.diff_to_pull_request_summary = dspy.ChainOfThought(
|
|
14
|
+
PullRequestSummaryGeneratorSignature
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
def forward(
|
|
18
|
+
self,
|
|
19
|
+
commits: str,
|
|
20
|
+
related_issues: str | None = None,
|
|
21
|
+
template: str | None = None,
|
|
22
|
+
):
|
|
23
|
+
if template:
|
|
24
|
+
rules = TEMPLATE_RULES
|
|
25
|
+
elif not related_issues or not related_issues.strip():
|
|
26
|
+
rules = RULES[2:]
|
|
27
|
+
else:
|
|
28
|
+
rules = RULES
|
|
29
|
+
result = self.diff_to_pull_request_summary(
|
|
30
|
+
commits=commits,
|
|
31
|
+
rules=rules,
|
|
32
|
+
related_issues=related_issues,
|
|
33
|
+
template=template,
|
|
34
|
+
)
|
|
35
|
+
return result
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Services layer for business logic."""
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""PR Summary generation service - business logic layer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from usepr.modules.pull_request_summary_generator import (
|
|
9
|
+
PullRequestSummaryGeneratorModule,
|
|
10
|
+
)
|
|
11
|
+
from usepr.utils.git import (
|
|
12
|
+
ensure_git_repo,
|
|
13
|
+
get_commits_between,
|
|
14
|
+
get_default_branch,
|
|
15
|
+
parse_commits,
|
|
16
|
+
resolve_ref,
|
|
17
|
+
)
|
|
18
|
+
from usepr.utils.github import PrTemplate, find_pr_templates
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class SummaryResult:
|
|
23
|
+
"""Result from PR summary generation."""
|
|
24
|
+
|
|
25
|
+
reasoning: str
|
|
26
|
+
summary: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class CommitContext:
|
|
31
|
+
"""Context about commits for summary generation."""
|
|
32
|
+
|
|
33
|
+
commits: list[str]
|
|
34
|
+
base_branch: str
|
|
35
|
+
default_branch: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_default_branch_name(repo_path: str) -> str:
|
|
39
|
+
"""Get the default branch name for the repository.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
repo_path: Absolute path to the repository.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
The default branch name (e.g., 'main' or 'master').
|
|
46
|
+
"""
|
|
47
|
+
return get_default_branch(repo_path).strip()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def gather_commits(repo_path: str, base_branch: str) -> CommitContext:
|
|
51
|
+
"""Gather commits between base branch and HEAD.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
repo_path: Absolute path to the repository.
|
|
55
|
+
base_branch: The base branch to diff against.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
CommitContext with parsed commits and branch info.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
SystemExit: If not a git repository or ref cannot be resolved.
|
|
62
|
+
"""
|
|
63
|
+
ensure_git_repo(repo_path)
|
|
64
|
+
|
|
65
|
+
default_branch = get_default_branch_name(repo_path)
|
|
66
|
+
prev_tag = resolve_ref(repo_path, base_branch)
|
|
67
|
+
|
|
68
|
+
commit_text = get_commits_between(repo_path, prev_tag, "HEAD")
|
|
69
|
+
commits = parse_commits(commit_text)
|
|
70
|
+
|
|
71
|
+
return CommitContext(
|
|
72
|
+
commits=commits,
|
|
73
|
+
base_branch=base_branch,
|
|
74
|
+
default_branch=default_branch,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def generate_summary(
|
|
79
|
+
commits: list[str],
|
|
80
|
+
related_issues: Optional[str] = None,
|
|
81
|
+
template: Optional[str] = None,
|
|
82
|
+
) -> SummaryResult:
|
|
83
|
+
"""Generate a PR summary from commits.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
commits: List of commit messages.
|
|
87
|
+
related_issues: Optional comma-separated related issues.
|
|
88
|
+
template: Optional PR template content.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
SummaryResult with reasoning and summary.
|
|
92
|
+
"""
|
|
93
|
+
program = PullRequestSummaryGeneratorModule()
|
|
94
|
+
result = program(
|
|
95
|
+
commits=commits,
|
|
96
|
+
related_issues=related_issues,
|
|
97
|
+
template=template,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
return SummaryResult(
|
|
101
|
+
reasoning=result.reasoning,
|
|
102
|
+
summary=result.summary,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_templates(repo_path: str) -> list[PrTemplate]:
|
|
107
|
+
"""Find PR templates in the repository.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
repo_path: Absolute path to the repository.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
List of found PR templates.
|
|
114
|
+
"""
|
|
115
|
+
return find_pr_templates(repo_path)
|
|
File without changes
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
import dspy
|
|
4
|
+
|
|
5
|
+
RULES = [
|
|
6
|
+
"Start the summary with a # Summary header.",
|
|
7
|
+
"The next section should be ## Linked Issues, listing any related issues or tasks.",
|
|
8
|
+
"Follow with a ## Description section that details the changes made in the pull request.",
|
|
9
|
+
"Use bullet points for each major change.",
|
|
10
|
+
"Keep the summary concise and to the point.",
|
|
11
|
+
"Highlight any breaking changes or important updates.",
|
|
12
|
+
"Avoid technical jargon; use clear and simple language.",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
TEMPLATE_RULES = [
|
|
16
|
+
"The output MUST follow the structure of the provided PR template.",
|
|
17
|
+
"Fill in every section of the template with relevant content derived from the commits.",
|
|
18
|
+
"Preserve the template's headings, checkbox placeholders, and formatting exactly.",
|
|
19
|
+
"If a template section does not apply, write 'N/A' rather than omitting it.",
|
|
20
|
+
"Do not add sections that are not in the template.",
|
|
21
|
+
"If no template is provided, use the default summary rules instead.",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PullRequestSummaryGeneratorSignature(dspy.Signature):
|
|
26
|
+
"""Generate a concise summary of a pull request based on a list of conventional commit messages. The summary should highlight the main changes introduced by the commits, and may include markdown tables or mermaid diagrams for better visualization when appropriate."""
|
|
27
|
+
|
|
28
|
+
rules: List[str] = dspy.InputField(
|
|
29
|
+
desc="A list of rules to follow when generating the summary."
|
|
30
|
+
)
|
|
31
|
+
commits: List[str] = dspy.InputField(desc="A list of conventional commit messages.")
|
|
32
|
+
related_issues: Optional[str] = dspy.InputField(
|
|
33
|
+
default=None, desc="A list of related issues or tasks, if any."
|
|
34
|
+
)
|
|
35
|
+
template: Optional[str] = dspy.InputField(
|
|
36
|
+
default=None,
|
|
37
|
+
desc="A GitHub pull request template to fill out. When provided, the summary must follow this template's structure and fill in each section.",
|
|
38
|
+
)
|
|
39
|
+
reasoning: str = dspy.OutputField(
|
|
40
|
+
desc="Step-by-step reasoning about the commits and how they contribute to the summary."
|
|
41
|
+
)
|
|
42
|
+
summary: str = dspy.OutputField(
|
|
43
|
+
desc="A summary of the pull request based on the provided commits, following the template structure if one was provided."
|
|
44
|
+
)
|
usepr/utils/__init__.py
ADDED
|
File without changes
|
usepr/utils/git.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import sys
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
from usecli import console
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def run(cmd: list[str], cwd: Optional[str] = None) -> str:
|
|
9
|
+
"""
|
|
10
|
+
Run a command and return its output as a string.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
cmd: List of command arguments.
|
|
14
|
+
cwd: Optional working directory.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
The command output as a string.
|
|
18
|
+
"""
|
|
19
|
+
return (
|
|
20
|
+
subprocess.check_output(cmd, stderr=subprocess.STDOUT, cwd=cwd)
|
|
21
|
+
.decode("utf-8", errors="replace")
|
|
22
|
+
.strip()
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def ensure_git_repo(repo_path: str) -> None:
|
|
27
|
+
"""
|
|
28
|
+
Ensure the given path is a git repository.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
repo_path: Path to the repository.
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
SystemExit: If not a git repository.
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
run(["git", "rev-parse", "--is-inside-work-tree"], cwd=repo_path)
|
|
38
|
+
except subprocess.CalledProcessError:
|
|
39
|
+
sys.stderr.write(f"[error] Not a git repository: {repo_path}\n")
|
|
40
|
+
sys.exit(2)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_commits_between(
|
|
44
|
+
repo_path: str, prev_tag: Optional[str], latest_tag: str = "HEAD"
|
|
45
|
+
) -> str:
|
|
46
|
+
"""
|
|
47
|
+
Get commit messages between two tags.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
repo_path: Path to the repository.
|
|
51
|
+
prev_tag: Previous tag, or None to use default branch.
|
|
52
|
+
latest_tag: Latest tag.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Formatted commit messages as string.
|
|
56
|
+
"""
|
|
57
|
+
# Format: subject, newline, newline, body, separator
|
|
58
|
+
fmt = "%s%n%n%b----"
|
|
59
|
+
if prev_tag is None:
|
|
60
|
+
prev_tag = get_default_branch(repo_path)
|
|
61
|
+
rng = f"{prev_tag}..{latest_tag}"
|
|
62
|
+
result = subprocess.run(
|
|
63
|
+
["git", "log", "--no-merges", f"--format={fmt}", rng],
|
|
64
|
+
cwd=repo_path,
|
|
65
|
+
capture_output=True,
|
|
66
|
+
text=True,
|
|
67
|
+
check=True,
|
|
68
|
+
)
|
|
69
|
+
text = result.stdout.strip()
|
|
70
|
+
# Remove trailing separator
|
|
71
|
+
if text.endswith("----"):
|
|
72
|
+
text = text[:-4].rstrip()
|
|
73
|
+
return text
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_default_branch(repo_path: str) -> str:
|
|
77
|
+
"""Get the default branch of the repository (main or master)."""
|
|
78
|
+
try:
|
|
79
|
+
result = subprocess.run(
|
|
80
|
+
["git", "symbolic-ref", "refs/remotes/origin/HEAD"],
|
|
81
|
+
cwd=repo_path,
|
|
82
|
+
capture_output=True,
|
|
83
|
+
text=True,
|
|
84
|
+
check=True,
|
|
85
|
+
)
|
|
86
|
+
return result.stdout.strip().split("/")[-1]
|
|
87
|
+
except subprocess.CalledProcessError:
|
|
88
|
+
# Fallback: check if 'main' exists, else 'master'
|
|
89
|
+
try:
|
|
90
|
+
subprocess.run(
|
|
91
|
+
["git", "show-ref", "--verify", "--quiet", "refs/heads/main"],
|
|
92
|
+
cwd=repo_path,
|
|
93
|
+
check=True,
|
|
94
|
+
)
|
|
95
|
+
return "main"
|
|
96
|
+
except subprocess.CalledProcessError:
|
|
97
|
+
return "master"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def resolve_ref(repo_path: str, ref: str) -> str:
|
|
101
|
+
"""Resolve a ref, trying the bare name first then origin/<ref>."""
|
|
102
|
+
for candidate in [ref, f"origin/{ref}"]:
|
|
103
|
+
result = subprocess.run(
|
|
104
|
+
["git", "rev-parse", "--verify", candidate],
|
|
105
|
+
cwd=repo_path,
|
|
106
|
+
capture_output=True,
|
|
107
|
+
)
|
|
108
|
+
if result.returncode == 0:
|
|
109
|
+
return candidate
|
|
110
|
+
console.print(
|
|
111
|
+
f"[bold red]Error: Could not resolve ref '{ref}'. Make sure the branch exists locally or on the remote.[/bold red]"
|
|
112
|
+
)
|
|
113
|
+
sys.exit(1)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def parse_commits(commit_text: str) -> List[str]:
|
|
117
|
+
"""
|
|
118
|
+
Parse the commit output into a list of individual commit messages.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
commit_text: The formatted commit messages string from get_commits_between.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
List of individual commit messages.
|
|
125
|
+
"""
|
|
126
|
+
return [commit.strip() for commit in commit_text.split("----") if commit.strip()]
|
usepr/utils/github.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Utilities for detecting and reading GitHub PR templates."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class PrTemplate:
|
|
12
|
+
"""A detected PR template."""
|
|
13
|
+
|
|
14
|
+
path: str
|
|
15
|
+
content: str
|
|
16
|
+
name: Optional[str] = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
TEMPLATE_FILENAMES = ["PULL_REQUEST_TEMPLATE.md"]
|
|
20
|
+
|
|
21
|
+
TEMPLATE_DIR_NAME = "PULL_REQUEST_TEMPLATE"
|
|
22
|
+
|
|
23
|
+
GITHUB_DIR = ".github"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _read_text(path: Path) -> Optional[str]:
|
|
27
|
+
"""Read a text file, returning None if unreadable."""
|
|
28
|
+
try:
|
|
29
|
+
return path.read_text(encoding="utf-8").strip()
|
|
30
|
+
except (OSError, UnicodeDecodeError):
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def find_pr_templates(repo_path: str) -> List[PrTemplate]:
|
|
35
|
+
"""
|
|
36
|
+
Find all PR templates in the repository.
|
|
37
|
+
|
|
38
|
+
Searches the following locations in order:
|
|
39
|
+
1. .github/PULL_REQUEST_TEMPLATE.md
|
|
40
|
+
2. .github/PULL_REQUEST_TEMPLATE/*.md (all .md files in the directory)
|
|
41
|
+
3. PULL_REQUEST_TEMPLATE.md (repo root)
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
repo_path: Absolute path to the repository root.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
List of PrTemplate objects found. Empty list if none found.
|
|
48
|
+
"""
|
|
49
|
+
root = Path(repo_path)
|
|
50
|
+
templates: List[PrTemplate] = []
|
|
51
|
+
|
|
52
|
+
# 1. .github/PULL_REQUEST_TEMPLATE.md
|
|
53
|
+
github_dir = root / GITHUB_DIR
|
|
54
|
+
for filename in TEMPLATE_FILENAMES:
|
|
55
|
+
candidate = github_dir / filename
|
|
56
|
+
content = _read_text(candidate)
|
|
57
|
+
if content:
|
|
58
|
+
templates.append(
|
|
59
|
+
PrTemplate(path=str(candidate), content=content, name=None)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# 2. .github/PULL_REQUEST_TEMPLATE/*.md
|
|
63
|
+
template_dir = github_dir / TEMPLATE_DIR_NAME
|
|
64
|
+
if template_dir.is_dir():
|
|
65
|
+
for md_file in sorted(template_dir.glob("*.md")):
|
|
66
|
+
content = _read_text(md_file)
|
|
67
|
+
if content:
|
|
68
|
+
templates.append(
|
|
69
|
+
PrTemplate(
|
|
70
|
+
path=str(md_file),
|
|
71
|
+
content=content,
|
|
72
|
+
name=md_file.stem,
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# 3. Root-level PULL_REQUEST_TEMPLATE.md
|
|
77
|
+
for filename in TEMPLATE_FILENAMES:
|
|
78
|
+
candidate = root / filename
|
|
79
|
+
content = _read_text(candidate)
|
|
80
|
+
if content:
|
|
81
|
+
templates.append(
|
|
82
|
+
PrTemplate(path=str(candidate), content=content, name=None)
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return templates
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: usepr
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author: Edward Boswell
|
|
6
|
+
Author-email: Edward Boswell <thememium@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Requires-Dist: dspy>=3.2.1
|
|
13
|
+
Requires-Dist: pyperclip>=1.11.0
|
|
14
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
15
|
+
Requires-Dist: rich>=15.0.0
|
|
16
|
+
Requires-Dist: typer>=0.26.7
|
|
17
|
+
Requires-Dist: usecli>=0.1.67
|
|
18
|
+
Requires-Python: >=3.13
|
|
19
|
+
Project-URL: Homepage, https://github.com/thememium/usepr
|
|
20
|
+
Project-URL: Documentation, https://github.com/thememium/usepr
|
|
21
|
+
Project-URL: Repository, https://github.com/thememium/usepr.git
|
|
22
|
+
Project-URL: Issues, https://github.com/thememium/usepr/issues
|
|
23
|
+
Project-URL: Changelog, https://github.com/thememium/usepr/blob/master/CHANGELOG.md
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
<a name="readme-top"></a>
|
|
27
|
+
|
|
28
|
+
<div align="center">
|
|
29
|
+
<a href="https://github.com/thememium/usepr">
|
|
30
|
+
<img src="https://raw.githubusercontent.com/thememium/usepr/refs/heads/master/docs/images/usepr-logo-dark-bg.png" alt="usePR" width="360" height="162">
|
|
31
|
+
</a>
|
|
32
|
+
|
|
33
|
+
<p align="center">
|
|
34
|
+
<em>AI-powered PR summary generator for Git repositories</em>
|
|
35
|
+
</p>
|
|
36
|
+
|
|
37
|
+
<p align="center">
|
|
38
|
+
<a href="#table-of-contents"><strong>Explore the Documentation »</strong></a>
|
|
39
|
+
<br />
|
|
40
|
+
<a href="https://github.com/thememium/usepr/issues">Report Bug</a>
|
|
41
|
+
·
|
|
42
|
+
<a href="https://github.com/thememium/usepr/issues">Request Feature</a>
|
|
43
|
+
</p>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<!-- TABLE OF CONTENTS -->
|
|
47
|
+
|
|
48
|
+
<a name="table-of-contents"></a>
|
|
49
|
+
|
|
50
|
+
<details>
|
|
51
|
+
<summary>Table of Contents</summary>
|
|
52
|
+
<ol>
|
|
53
|
+
<li><a href="#about">About</a></li>
|
|
54
|
+
<li><a href="#quick-start">Quick Start</a></li>
|
|
55
|
+
<li><a href="#usage">Usage</a></li>
|
|
56
|
+
<li><a href="#development">Development</a></li>
|
|
57
|
+
<li><a href="#contributing">Contributing</a></li>
|
|
58
|
+
<li><a href="#license">License</a></li>
|
|
59
|
+
</ol>
|
|
60
|
+
</details>
|
|
61
|
+
|
|
62
|
+
<!-- ABOUT -->
|
|
63
|
+
|
|
64
|
+
## About
|
|
65
|
+
|
|
66
|
+
usepr (`usepr`) is a Python CLI that generates pull request summaries from your git commits using AI (DSPy). It analyzes your commit history and produces well-structured, meaningful PR descriptions.
|
|
67
|
+
|
|
68
|
+
- **AI-powered summaries** - Uses DSPy ChainOfThought to understand and summarize your changes
|
|
69
|
+
- **Template support** - Automatically detects and uses PR templates from your repository
|
|
70
|
+
- **Flexible diffing** - Generate summaries between any branches, tags, or commits
|
|
71
|
+
- **Interactive prompts** - Guided workflow with base branch and issue selection
|
|
72
|
+
- **Clipboard integration** - Copy generated summaries directly to clipboard
|
|
73
|
+
- **Model override** - Use different LLM models via the `-m` flag
|
|
74
|
+
|
|
75
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
76
|
+
|
|
77
|
+
<!-- QUICK START -->
|
|
78
|
+
|
|
79
|
+
## Quick Start
|
|
80
|
+
|
|
81
|
+
### Install usepr with uv (recommended)
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
uv tool install usepr
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Install with pipx (alternative)
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
pipx install usepr
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Generate a PR summary
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
usepr generate
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
This will prompt you to select a base branch, optionally link related issues, and generate a summary from your commits.
|
|
100
|
+
|
|
101
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
102
|
+
|
|
103
|
+
<!-- USAGE -->
|
|
104
|
+
|
|
105
|
+
## Usage
|
|
106
|
+
|
|
107
|
+
### Generate a PR Summary
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
usepr generate
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The interactive workflow will:
|
|
114
|
+
|
|
115
|
+
1. Detect your repository's default branch
|
|
116
|
+
2. Prompt for a base branch to diff against
|
|
117
|
+
3. Gather commits between base and HEAD
|
|
118
|
+
4. Ask for related issues (optional)
|
|
119
|
+
5. Detect and offer PR templates (if any)
|
|
120
|
+
6. Generate and display the summary
|
|
121
|
+
7. Offer to copy to clipboard
|
|
122
|
+
|
|
123
|
+
### Use a Custom Model
|
|
124
|
+
|
|
125
|
+
Set your API key as an environment variable for the provider you're using:
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
export OPENAI_API_KEY="sk-..."
|
|
129
|
+
export ANTHROPIC_API_KEY="sk-ant-..."
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Then run with the `provider/model` format:
|
|
133
|
+
|
|
134
|
+
```sh
|
|
135
|
+
usepr generate -m openai/gpt-4o
|
|
136
|
+
usepr generate -m anthropic/claude-sonnet-4-20250514
|
|
137
|
+
usepr generate -m openrouter/google/gemini-2.5-flash
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Use the Short Alias
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
usepr gen
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Available Commands
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
generate (gen) Generate a PR summary from commits
|
|
150
|
+
help Show help
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
154
|
+
|
|
155
|
+
<!-- DEVELOPMENT -->
|
|
156
|
+
|
|
157
|
+
## Development
|
|
158
|
+
|
|
159
|
+
Common tasks:
|
|
160
|
+
|
|
161
|
+
```sh
|
|
162
|
+
uv run poe clean-full
|
|
163
|
+
uv run poe test
|
|
164
|
+
uv run poe lint
|
|
165
|
+
uv run poe format
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
169
|
+
|
|
170
|
+
<!-- CONTRIBUTING -->
|
|
171
|
+
|
|
172
|
+
## Contributing
|
|
173
|
+
|
|
174
|
+
Quick workflow:
|
|
175
|
+
|
|
176
|
+
1. Fork and branch: `git checkout -b feature/name`
|
|
177
|
+
2. Make changes
|
|
178
|
+
3. Run checks: `uv run poe clean-full`
|
|
179
|
+
4. Commit and push
|
|
180
|
+
5. Open a Pull Request
|
|
181
|
+
|
|
182
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
183
|
+
|
|
184
|
+
<!-- LICENSE -->
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
License information has not been added yet.
|
|
189
|
+
|
|
190
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
<div align="center">
|
|
195
|
+
<p>
|
|
196
|
+
<sub>Built by <a href="https://github.com/thememium">thememium</a></sub>
|
|
197
|
+
</p>
|
|
198
|
+
</div>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
usepr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
usepr/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
usepr/cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
usepr/cli/commands/generate_command.py,sha256=-ouv_Mg-PoPTqoa8NrawC0_Ht82R_fy09ynN90PHsC4,5309
|
|
5
|
+
usepr/cli/templates/command.py.j2,sha256=glpKmz98KtFTZmmYtPYUeFrUqUfRJTFCYIQMKnaPQ-M,2322
|
|
6
|
+
usepr/cli/themes/default.toml,sha256=tNtI6yTUgbnTevSO2Q9RN06oH1FXs0H7_cAfIB8GY0I,583
|
|
7
|
+
usepr/cli/themes/title.txt,sha256=JiHOBLFhhhnd0c_jngSj2JqXV0-yaF61L3nRavCmsWs,619
|
|
8
|
+
usepr/cli/usecli.config.toml,sha256=63P9KMcDIRXtS22TsJb6S8Opcv1xt6MNCkMPur3xrkA,278
|
|
9
|
+
usepr/configs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
usepr/configs/dspy.py,sha256=w1KnqU6zA-5LMh0nrsc3I0_VzPqmko3UCSC1cE4cQDM,1476
|
|
11
|
+
usepr/modules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
usepr/modules/pull_request_summary_generator.py,sha256=l2F4nTt1CHe8llxQVI3gihafpvtU5VVhLTPTZO4LXjE,948
|
|
13
|
+
usepr/services/__init__.py,sha256=kMCe_42FMbwfHA6AT5yB7O2KB-mB89K46ZOamy-40Ak,41
|
|
14
|
+
usepr/services/pr_summary_service.py,sha256=LTOXcmgm79FMS0YJ2m1REL_1iXTHUOPXT2C1fRTTHkQ,2781
|
|
15
|
+
usepr/signatures/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
usepr/signatures/pull_request_summary_generator.py,sha256=1uMweJNqNLOk2DBBK2lhKUPi0W-TcTBJM8QUWdh6jhk,2214
|
|
17
|
+
usepr/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
+
usepr/utils/git.py,sha256=Nn3DSDuprgqGvB4AldCiVT3YYLmJAC6DEpDP7tHZ_eo,3581
|
|
19
|
+
usepr/utils/github.py,sha256=LW1ZHpv2nVD0qejYMqw0VUwknv4oZ1pDARKk0avyky0,2369
|
|
20
|
+
usepr-0.1.2.dist-info/licenses/LICENSE,sha256=3URXXqgUcmPG_29EcFLqyGt7EWNGU709amKh90jCQIA,1081
|
|
21
|
+
usepr-0.1.2.dist-info/WHEEL,sha256=8ZlpUMJ7mlDirmlHRhDirEx_nPnARrwDjeE92mlk68E,81
|
|
22
|
+
usepr-0.1.2.dist-info/entry_points.txt,sha256=sVOvvsPgBzAE5AHTqZ9EqwKy5Bcc50bvlgUvHUf-_nQ,39
|
|
23
|
+
usepr-0.1.2.dist-info/METADATA,sha256=9fQD_N4u2DAtgeMy4b1qNs-iNaFpuGWnqAwwnPqEyQY,4872
|
|
24
|
+
usepr-0.1.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026–present Edward Boswell
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|