Skip to content

Commit cbaaa3f

Browse files
committed
refactor: split git suggestion helpers
Separate commit-message prompting, PR-title prompting, shared LLM requests, and title parsing so each suggestion flow can evolve independently. Made-with: Cursor
1 parent a875aef commit cbaaa3f

File tree

6 files changed

+147
-129
lines changed

6 files changed

+147
-129
lines changed

TODO.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@
8383
- [x] `src/commands/feedback_eval/input/conversion.rs`: split review-session conversion from label normalization helpers.
8484
- [x] `src/commands/pr.rs`: separate summary-only flow, full review flow, and comment-posting orchestration.
8585
- [x] `src/commands/pr/gh.rs`: carve PR resolution, diff fetching, and metadata fetching.
86-
- [ ] `src/commands/git/suggest.rs`: split commit-message prompting from PR-title prompting and response extraction.
86+
- [x] `src/commands/git/suggest.rs`: split commit-message prompting from PR-title prompting and response extraction.
8787
- [ ] `src/commands/review/command.rs`: split review/check/compare entrypoints if they keep diverging.
8888
- [ ] `src/commands/misc/feedback/command.rs`: separate file loading/ID normalization from store persistence.
8989
- [ ] `src/commands/misc/feedback/apply.rs`: split acceptance/rejection counters from store mutation helpers.

src/commands/git/suggest.rs

Lines changed: 11 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -1,128 +1,11 @@
1-
use anyhow::Result;
2-
3-
use crate::adapters;
4-
use crate::config;
5-
use crate::core;
6-
7-
pub(super) async fn suggest_commit_message(config: config::Config) -> Result<()> {
8-
let git = core::GitIntegration::new(".")?;
9-
let diff_content = git.get_staged_diff()?;
10-
11-
if diff_content.is_empty() {
12-
println!("No staged changes found. Stage your changes with 'git add' first.");
13-
return Ok(());
14-
}
15-
16-
let model_config = config.to_model_config_for_role(config::ModelRole::Fast);
17-
let adapter = adapters::llm::create_adapter(&model_config)?;
18-
19-
let (system_prompt, user_prompt) =
20-
core::CommitPromptBuilder::build_commit_prompt(&diff_content);
21-
22-
let request = adapters::llm::LLMRequest {
23-
system_prompt,
24-
user_prompt,
25-
temperature: Some(0.3),
26-
max_tokens: Some(500),
27-
response_schema: None,
28-
};
29-
30-
let response = adapter.complete(request).await?;
31-
let commit_message = core::CommitPromptBuilder::extract_commit_message(&response.content);
32-
33-
println!("\nSuggested commit message:");
34-
println!("{}", commit_message);
35-
36-
if commit_message.len() > 72 {
37-
println!(
38-
"\n⚠️ Warning: Commit message exceeds 72 characters ({})",
39-
commit_message.len()
40-
);
41-
}
42-
43-
Ok(())
44-
}
45-
46-
pub(super) async fn suggest_pr_title(config: config::Config) -> Result<()> {
47-
let git = core::GitIntegration::new(".")?;
48-
let base_branch = git
49-
.get_default_branch()
50-
.unwrap_or_else(|_| "main".to_string());
51-
let diff_content = git.get_branch_diff(&base_branch)?;
52-
53-
if diff_content.is_empty() {
54-
println!("No changes found compared to {} branch.", base_branch);
55-
return Ok(());
56-
}
57-
58-
let model_config = config.to_model_config_for_role(config::ModelRole::Fast);
59-
let adapter = adapters::llm::create_adapter(&model_config)?;
60-
61-
let (system_prompt, user_prompt) =
62-
core::CommitPromptBuilder::build_pr_title_prompt(&diff_content);
63-
64-
let request = adapters::llm::LLMRequest {
65-
system_prompt,
66-
user_prompt,
67-
temperature: Some(0.3),
68-
max_tokens: Some(200),
69-
response_schema: None,
70-
};
71-
72-
let response = adapter.complete(request).await?;
73-
let title = extract_title_from_response(&response.content);
74-
75-
println!("\nSuggested PR title:");
76-
println!("{}", title);
77-
78-
if title.len() > 65 {
79-
println!(
80-
"\n⚠️ Warning: PR title exceeds 65 characters ({})",
81-
title.len()
82-
);
83-
}
84-
85-
Ok(())
86-
}
87-
88-
fn extract_title_from_response(content: &str) -> String {
89-
if let Some(start) = content.find("<title>") {
90-
let after_tag = start + 7;
91-
if let Some(end) = content[after_tag..].find("</title>") {
92-
content[after_tag..after_tag + end].trim().to_string()
93-
} else {
94-
content.trim().to_string()
95-
}
96-
} else {
97-
content
98-
.lines()
99-
.find(|line| !line.trim().is_empty())
100-
.unwrap_or("")
101-
.trim()
102-
.to_string()
103-
}
104-
}
105-
106-
#[cfg(test)]
107-
mod tests {
108-
use super::*;
109-
110-
#[test]
111-
fn test_extract_title_normal() {
112-
let content = "<title>Fix login bug</title>";
113-
assert_eq!(extract_title_from_response(content), "Fix login bug");
114-
}
115-
116-
#[test]
117-
fn test_extract_title_malformed_closing_before_opening() {
118-
let content = "Some text</title> more <title>Real Title</title>";
119-
let title = extract_title_from_response(content);
120-
assert!(!title.is_empty());
121-
}
122-
123-
#[test]
124-
fn test_extract_title_no_tags() {
125-
let content = "Just a plain title\nSecond line";
126-
assert_eq!(extract_title_from_response(content), "Just a plain title");
127-
}
128-
}
1+
#[path = "suggest/commit.rs"]
2+
mod commit;
3+
#[path = "suggest/pr_title.rs"]
4+
mod pr_title;
5+
#[path = "suggest/request.rs"]
6+
mod request;
7+
#[path = "suggest/response.rs"]
8+
mod response;
9+
10+
pub(super) use commit::suggest_commit_message;
11+
pub(super) use pr_title::suggest_pr_title;

src/commands/git/suggest/commit.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
use anyhow::Result;
2+
3+
use crate::config;
4+
use crate::core;
5+
6+
use super::request::complete_suggestion;
7+
8+
pub(in super::super) async fn suggest_commit_message(config: config::Config) -> Result<()> {
9+
let git = core::GitIntegration::new(".")?;
10+
let diff_content = git.get_staged_diff()?;
11+
12+
if diff_content.is_empty() {
13+
println!("No staged changes found. Stage your changes with 'git add' first.");
14+
return Ok(());
15+
}
16+
17+
let (system_prompt, user_prompt) =
18+
core::CommitPromptBuilder::build_commit_prompt(&diff_content);
19+
let response = complete_suggestion(&config, system_prompt, user_prompt, 500).await?;
20+
let commit_message = core::CommitPromptBuilder::extract_commit_message(&response);
21+
22+
println!("\nSuggested commit message:");
23+
println!("{}", commit_message);
24+
25+
if commit_message.len() > 72 {
26+
println!(
27+
"\nWarning: Commit message exceeds 72 characters ({})",
28+
commit_message.len()
29+
);
30+
}
31+
32+
Ok(())
33+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
use anyhow::Result;
2+
3+
use crate::config;
4+
use crate::core;
5+
6+
use super::request::complete_suggestion;
7+
use super::response::extract_title_from_response;
8+
9+
pub(in super::super) async fn suggest_pr_title(config: config::Config) -> Result<()> {
10+
let git = core::GitIntegration::new(".")?;
11+
let base_branch = git
12+
.get_default_branch()
13+
.unwrap_or_else(|_| "main".to_string());
14+
let diff_content = git.get_branch_diff(&base_branch)?;
15+
16+
if diff_content.is_empty() {
17+
println!("No changes found compared to {} branch.", base_branch);
18+
return Ok(());
19+
}
20+
21+
let (system_prompt, user_prompt) =
22+
core::CommitPromptBuilder::build_pr_title_prompt(&diff_content);
23+
let response = complete_suggestion(&config, system_prompt, user_prompt, 200).await?;
24+
let title = extract_title_from_response(&response);
25+
26+
println!("\nSuggested PR title:");
27+
println!("{}", title);
28+
29+
if title.len() > 65 {
30+
println!(
31+
"\nWarning: PR title exceeds 65 characters ({})",
32+
title.len()
33+
);
34+
}
35+
36+
Ok(())
37+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
use anyhow::Result;
2+
3+
use crate::adapters;
4+
use crate::config;
5+
6+
pub(super) async fn complete_suggestion(
7+
config: &config::Config,
8+
system_prompt: String,
9+
user_prompt: String,
10+
max_tokens: usize,
11+
) -> Result<String> {
12+
let model_config = config.to_model_config_for_role(config::ModelRole::Fast);
13+
let adapter = adapters::llm::create_adapter(&model_config)?;
14+
let request = adapters::llm::LLMRequest {
15+
system_prompt,
16+
user_prompt,
17+
temperature: Some(0.3),
18+
max_tokens: Some(max_tokens),
19+
response_schema: None,
20+
};
21+
22+
let response = adapter.complete(request).await?;
23+
Ok(response.content)
24+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
pub(super) fn extract_title_from_response(content: &str) -> String {
2+
if let Some(start) = content.find("<title>") {
3+
let after_tag = start + 7;
4+
if let Some(end) = content[after_tag..].find("</title>") {
5+
content[after_tag..after_tag + end].trim().to_string()
6+
} else {
7+
content.trim().to_string()
8+
}
9+
} else {
10+
content
11+
.lines()
12+
.find(|line| !line.trim().is_empty())
13+
.unwrap_or("")
14+
.trim()
15+
.to_string()
16+
}
17+
}
18+
19+
#[cfg(test)]
20+
mod tests {
21+
use super::*;
22+
23+
#[test]
24+
fn test_extract_title_normal() {
25+
let content = "<title>Fix login bug</title>";
26+
assert_eq!(extract_title_from_response(content), "Fix login bug");
27+
}
28+
29+
#[test]
30+
fn test_extract_title_malformed_closing_before_opening() {
31+
let content = "Some text</title> more <title>Real Title</title>";
32+
let title = extract_title_from_response(content);
33+
assert!(!title.is_empty());
34+
}
35+
36+
#[test]
37+
fn test_extract_title_no_tags() {
38+
let content = "Just a plain title\nSecond line";
39+
assert_eq!(extract_title_from_response(content), "Just a plain title");
40+
}
41+
}

0 commit comments

Comments
 (0)