Skip to content

Commit 1f7edd0

Browse files
Copilotoleander
andcommitted
Implement parallel git diff analysis algorithm
Co-authored-by: oleander <220827+oleander@users.noreply.github.com>
1 parent 5c4b7b3 commit 1f7edd0

File tree

3 files changed

+269
-12
lines changed

3 files changed

+269
-12
lines changed

src/commit.rs

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use async_openai::Client;
66
use crate::{config, debug_output, openai, profile};
77
use crate::model::Model;
88
use crate::config::AppConfig;
9-
use crate::multi_step_integration::{generate_commit_message_local, generate_commit_message_multi_step};
9+
use crate::multi_step_integration::{generate_commit_message_local, generate_commit_message_multi_step, generate_commit_message_parallel};
1010

1111
/// The instruction template included at compile time
1212
const INSTRUCTION_TEMPLATE: &str = include_str!("../resources/prompt.md");
@@ -117,16 +117,25 @@ pub async fn generate(patch: String, remaining_tokens: usize, model: Model, sett
117117
let client = Client::with_config(config);
118118
let model_str = model.to_string();
119119

120-
match generate_commit_message_multi_step(&client, &model_str, &patch, max_length).await {
120+
// Try parallel approach first
121+
match generate_commit_message_parallel(&client, &model_str, &patch, max_length).await {
121122
Ok(message) => return Ok(openai::Response { response: message }),
122123
Err(e) => {
123124
// Check if it's an API key error
124125
if e.to_string().contains("invalid_api_key") || e.to_string().contains("Incorrect API key") {
125126
bail!("Invalid OpenAI API key. Please check your API key configuration.");
126127
}
127-
log::warn!("Multi-step generation with custom settings failed: {e}");
128-
if let Some(session) = debug_output::debug_session() {
129-
session.set_multi_step_error(e.to_string());
128+
log::warn!("Parallel generation with custom settings failed, trying multi-step: {e}");
129+
130+
// Fallback to old multi-step approach
131+
match generate_commit_message_multi_step(&client, &model_str, &patch, max_length).await {
132+
Ok(message) => return Ok(openai::Response { response: message }),
133+
Err(e2) => {
134+
log::warn!("Multi-step generation with custom settings also failed: {e2}");
135+
if let Some(session) = debug_output::debug_session() {
136+
session.set_multi_step_error(e2.to_string());
137+
}
138+
}
130139
}
131140
}
132141
}
@@ -145,16 +154,25 @@ pub async fn generate(patch: String, remaining_tokens: usize, model: Model, sett
145154
let client = Client::new();
146155
let model_str = model.to_string();
147156

148-
match generate_commit_message_multi_step(&client, &model_str, &patch, max_length).await {
157+
// Try parallel approach first
158+
match generate_commit_message_parallel(&client, &model_str, &patch, max_length).await {
149159
Ok(message) => return Ok(openai::Response { response: message }),
150160
Err(e) => {
151161
// Check if it's an API key error
152162
if e.to_string().contains("invalid_api_key") || e.to_string().contains("Incorrect API key") {
153163
bail!("Invalid OpenAI API key. Please check your API key configuration.");
154164
}
155-
log::warn!("Multi-step generation failed: {e}");
156-
if let Some(session) = debug_output::debug_session() {
157-
session.set_multi_step_error(e.to_string());
165+
log::warn!("Parallel generation failed, trying multi-step: {e}");
166+
167+
// Fallback to old multi-step approach
168+
match generate_commit_message_multi_step(&client, &model_str, &patch, max_length).await {
169+
Ok(message) => return Ok(openai::Response { response: message }),
170+
Err(e2) => {
171+
log::warn!("Multi-step generation also failed: {e2}");
172+
if let Some(session) = debug_output::debug_session() {
173+
session.set_multi_step_error(e2.to_string());
174+
}
175+
}
158176
}
159177
}
160178
}

src/multi_step_integration.rs

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,174 @@ async fn select_best_candidate(
591591
}
592592
}
593593

594+
/// Optimized parallel approach for commit message generation
595+
/// This replaces the sequential multi-step approach with true parallel processing
596+
pub async fn generate_commit_message_parallel(
597+
client: &Client<OpenAIConfig>, model: &str, diff_content: &str, max_length: Option<usize>
598+
) -> Result<String> {
599+
log::info!("Starting parallel commit message generation");
600+
601+
// Parse the diff to extract individual files
602+
let parsed_files = parse_diff(diff_content)?;
603+
log::info!("Parsed {} files from diff", parsed_files.len());
604+
605+
if parsed_files.is_empty() {
606+
anyhow::bail!("No files found in diff");
607+
}
608+
609+
// Phase 1: Analyze each file in parallel using simplified approach
610+
log::debug!("Starting parallel analysis of {} files", parsed_files.len());
611+
612+
let analysis_futures: Vec<_> = parsed_files
613+
.iter()
614+
.map(|file| {
615+
let file_path = file.path.clone();
616+
let operation = file.operation.clone();
617+
let diff_content = file.diff_content.clone();
618+
async move {
619+
analyze_single_file_simple(client, model, &file_path, &operation, &diff_content).await
620+
}
621+
})
622+
.collect();
623+
624+
// Execute all file analyses concurrently
625+
let analysis_results = join_all(analysis_futures).await;
626+
627+
// Collect successful analyses
628+
let mut successful_analyses = Vec::new();
629+
for (i, result) in analysis_results.into_iter().enumerate() {
630+
match result {
631+
Ok(summary) => {
632+
log::debug!("Successfully analyzed file {}: {}", i, parsed_files[i].path);
633+
successful_analyses.push((parsed_files[i].path.clone(), summary));
634+
}
635+
Err(e) => {
636+
// Check if it's an API key error - if so, propagate immediately
637+
let error_str = e.to_string();
638+
if error_str.contains("invalid_api_key") || error_str.contains("Incorrect API key") || error_str.contains("Invalid API key") {
639+
return Err(e);
640+
}
641+
log::warn!("Failed to analyze file {}: {}", parsed_files[i].path, e);
642+
// Continue with other files
643+
}
644+
}
645+
}
646+
647+
if successful_analyses.is_empty() {
648+
anyhow::bail!("Failed to analyze any files in parallel");
649+
}
650+
651+
// Phase 2: Synthesize final commit message from all analyses
652+
log::debug!("Synthesizing final commit message from {} analyses", successful_analyses.len());
653+
654+
let synthesis_result = synthesize_commit_message(
655+
client,
656+
model,
657+
&successful_analyses,
658+
max_length.unwrap_or(72),
659+
).await?;
660+
661+
Ok(synthesis_result)
662+
}
663+
664+
/// Analyzes a single file using simplified text completion (no function calling)
665+
async fn analyze_single_file_simple(
666+
client: &Client<OpenAIConfig>,
667+
model: &str,
668+
file_path: &str,
669+
operation: &str,
670+
diff_content: &str,
671+
) -> Result<String> {
672+
let system_prompt = "You are a git diff analyzer. Analyze the provided file change and provide a concise summary in 1-2 sentences describing what changed and why it matters.";
673+
674+
let user_prompt = format!(
675+
"File: {}\nOperation: {}\nDiff:\n{}\n\nProvide a concise summary (1-2 sentences) of what changed and why it matters:",
676+
file_path, operation, diff_content
677+
);
678+
679+
let request = CreateChatCompletionRequestArgs::default()
680+
.model(model)
681+
.messages(vec![
682+
ChatCompletionRequestSystemMessageArgs::default()
683+
.content(system_prompt)
684+
.build()?
685+
.into(),
686+
ChatCompletionRequestUserMessageArgs::default()
687+
.content(user_prompt)
688+
.build()?
689+
.into(),
690+
])
691+
.max_tokens(150u32) // Keep responses concise
692+
.build()?;
693+
694+
let response = client.chat().create(request).await?;
695+
696+
let content = response.choices[0]
697+
.message
698+
.content
699+
.as_ref()
700+
.ok_or_else(|| anyhow::anyhow!("No content in response"))?;
701+
702+
Ok(content.trim().to_string())
703+
}
704+
705+
/// Synthesizes a final commit message from multiple file analyses
706+
async fn synthesize_commit_message(
707+
client: &Client<OpenAIConfig>,
708+
model: &str,
709+
analyses: &[(String, String)],
710+
max_length: usize,
711+
) -> Result<String> {
712+
// Build context from all analyses
713+
let mut context = String::new();
714+
context.push_str("File changes summary:\n");
715+
for (file_path, summary) in analyses {
716+
context.push_str(&format!("• {}: {}\n", file_path, summary));
717+
}
718+
719+
let system_prompt = format!(
720+
"You are a git commit message expert. Based on the file change summaries provided, generate a concise, descriptive commit message that captures the essential nature of the changes. The message should be {} characters or less and follow conventional commit format when appropriate. Focus on WHAT changed and WHY, not just listing files.",
721+
max_length
722+
);
723+
724+
let user_prompt = format!(
725+
"{}\n\nGenerate a commit message (max {} characters) that captures the essential nature of these changes:",
726+
context, max_length
727+
);
728+
729+
let request = CreateChatCompletionRequestArgs::default()
730+
.model(model)
731+
.messages(vec![
732+
ChatCompletionRequestSystemMessageArgs::default()
733+
.content(system_prompt)
734+
.build()?
735+
.into(),
736+
ChatCompletionRequestUserMessageArgs::default()
737+
.content(user_prompt)
738+
.build()?
739+
.into(),
740+
])
741+
.max_tokens(100u32) // Commit messages should be short
742+
.build()?;
743+
744+
let response = client.chat().create(request).await?;
745+
746+
let content = response.choices[0]
747+
.message
748+
.content
749+
.as_ref()
750+
.ok_or_else(|| anyhow::anyhow!("No content in response"))?;
751+
752+
let message = content.trim().to_string();
753+
754+
// Ensure message length doesn't exceed limit
755+
if message.len() > max_length {
756+
Ok(message.chars().take(max_length - 3).collect::<String>() + "...")
757+
} else {
758+
Ok(message)
759+
}
760+
}
761+
594762
/// Alternative: Use the multi-step analysis locally without OpenAI calls
595763
pub fn generate_commit_message_local(diff_content: &str, max_length: Option<usize>) -> Result<String> {
596764
use crate::multi_step_analysis::{analyze_file, calculate_impact_scores, generate_commit_messages};
@@ -807,4 +975,66 @@ index 1234567..abcdefg 100644
807975
assert!(!message.is_empty());
808976
assert!(message.len() <= 72);
809977
}
978+
979+
#[tokio::test]
980+
async fn test_parallel_generation_parsing() {
981+
// Test that the parallel approach correctly handles multi-file diffs
982+
let diff = r#"diff --git a/src/auth.rs b/src/auth.rs
983+
index 1234567..abcdefg 100644
984+
--- a/src/auth.rs
985+
+++ b/src/auth.rs
986+
@@ -1,3 +1,4 @@
987+
+use crate::security;
988+
pub fn authenticate() {
989+
// authentication logic
990+
}
991+
diff --git a/src/main.rs b/src/main.rs
992+
index abcd123..efgh456 100644
993+
--- a/src/main.rs
994+
+++ b/src/main.rs
995+
@@ -1,2 +1,3 @@
996+
fn main() {
997+
println!("Hello");
998+
+ auth::authenticate();
999+
}"#;
1000+
1001+
// Parse files to ensure parsing works correctly for parallel processing
1002+
let files = parse_diff(diff).unwrap();
1003+
assert_eq!(files.len(), 2);
1004+
assert_eq!(files[0].path, "src/auth.rs");
1005+
assert_eq!(files[1].path, "src/main.rs");
1006+
1007+
// Verify diff content is captured
1008+
assert!(files[0].diff_content.contains("use crate::security"));
1009+
assert!(files[1].diff_content.contains("auth::authenticate"));
1010+
}
1011+
1012+
#[test]
1013+
fn test_parse_diff_edge_cases() {
1014+
// Test parsing with various git prefixes and edge cases
1015+
let diff_with_dev_null = r#"diff --git a/old_file.txt b/dev/null
1016+
deleted file mode 100644
1017+
index 1234567..0000000
1018+
--- a/old_file.txt
1019+
+++ /dev/null
1020+
@@ -1,2 +0,0 @@
1021+
-Old content
1022+
-To be removed"#;
1023+
1024+
let files = parse_diff(diff_with_dev_null).unwrap();
1025+
assert_eq!(files.len(), 1);
1026+
assert_eq!(files[0].path, "old_file.txt", "Should extract original path for deleted files");
1027+
assert_eq!(files[0].operation, "deleted");
1028+
1029+
// Test with binary files
1030+
let diff_binary = r#"diff --git a/image.png b/image.png
1031+
new file mode 100644
1032+
index 0000000..1234567
1033+
Binary files /dev/null and b/image.png differ"#;
1034+
1035+
let files = parse_diff(diff_binary).unwrap();
1036+
assert_eq!(files.len(), 1);
1037+
assert_eq!(files[0].path, "image.png");
1038+
assert_eq!(files[0].operation, "binary");
1039+
}
8101040
}

src/openai.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use futures::future::join_all;
1111
use crate::{commit, config, debug_output, function_calling, profile};
1212
use crate::model::Model;
1313
use crate::config::AppConfig;
14-
use crate::multi_step_integration::generate_commit_message_multi_step;
14+
use crate::multi_step_integration::{generate_commit_message_multi_step, generate_commit_message_parallel};
1515

1616
const MAX_ATTEMPTS: usize = 3;
1717

@@ -205,14 +205,23 @@ pub async fn call_with_config(request: Request, config: OpenAIConfig) -> Result<
205205
let client = Client::with_config(config.clone());
206206
let model = request.model.to_string();
207207

208-
match generate_commit_message_multi_step(&client, &model, &request.prompt, config::APP_CONFIG.max_commit_length).await {
208+
// Try parallel approach first
209+
match generate_commit_message_parallel(&client, &model, &request.prompt, config::APP_CONFIG.max_commit_length).await {
209210
Ok(message) => return Ok(Response { response: message }),
210211
Err(e) => {
211212
// Check if it's an API key error and propagate it
212213
if e.to_string().contains("invalid_api_key") || e.to_string().contains("Incorrect API key") {
213214
return Err(e);
214215
}
215-
log::warn!("Multi-step approach failed, falling back to single-step: {e}");
216+
log::warn!("Parallel approach failed, trying multi-step: {e}");
217+
218+
// Fallback to old multi-step approach
219+
match generate_commit_message_multi_step(&client, &model, &request.prompt, config::APP_CONFIG.max_commit_length).await {
220+
Ok(message) => return Ok(Response { response: message }),
221+
Err(e2) => {
222+
log::warn!("Multi-step approach also failed, falling back to single-step: {e2}");
223+
}
224+
}
216225
}
217226
}
218227

0 commit comments

Comments
 (0)