diff --git a/src/git.rs b/src/git.rs index fd4167a48..b6b98a7ac 100644 --- a/src/git.rs +++ b/src/git.rs @@ -1,6 +1,42 @@ +use std::sync::Arc; +use tokio::sync::RwLock as ARwLock; use std::path::PathBuf; +use url::Url; +use serde::{Serialize, Deserialize}; use tracing::error; -use git2::{Branch, BranchType, DiffOptions, IndexAddOption, Oid, Repository, Signature, Status, StatusOptions}; +use git2::{Branch, DiffOptions, Oid, Repository, Signature, Status, StatusOptions}; + +use crate::global_context::GlobalContext; +use crate::agentic::generate_commit_message::generate_commit_message_by_diff; + +#[derive(Serialize, Deserialize, Debug)] +pub struct CommitInfo { + pub project_path: Url, + pub commit_message: String, + pub file_changes: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct FileChange { + pub path: String, + pub status: FileChangeStatus, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum FileChangeStatus { + ADDED, + MODIFIED, + DELETED, +} +impl FileChangeStatus { + pub fn initial(&self) -> char { + match self { + FileChangeStatus::ADDED => 'A', + FileChangeStatus::MODIFIED => 'M', + FileChangeStatus::DELETED => 'D', + } + } +} pub fn git_ls_files(repository_path: &PathBuf) -> Option> { let repository = Repository::open(repository_path) @@ -28,61 +64,91 @@ pub fn git_ls_files(repository_path: &PathBuf) -> Option> { } /// Similar to git checkout -b -pub fn create_or_checkout_to_branch<'repo>(repository: &'repo Repository, branch_name: &str) -> Result, String> { - let branch = match repository.find_branch(branch_name, BranchType::Local) { - Ok(branch) => branch, - Err(_) => { - let head_commit = repository.head() - .and_then(|h| h.peel_to_commit()) - .map_err(|e| format!("Failed to get HEAD commit: {}", e))?; - repository.branch(branch_name, &head_commit, false) - .map_err(|e| format!("Failed to create branch: {}", e))? - } - }; +// pub fn create_or_checkout_to_branch<'repo>(repository: &'repo Repository, branch_name: &str) -> Result, String> { +// let branch = match repository.find_branch(branch_name, git2::BranchType::Local) { +// Ok(branch) => branch, +// Err(_) => { +// let head_commit = repository.head() +// .and_then(|h| h.peel_to_commit()) +// .map_err(|e| format!("Failed to get HEAD commit: {}", e))?; +// repository.branch(branch_name, &head_commit, false) +// .map_err(|e| format!("Failed to create branch: {}", e))? +// } +// }; - // Checkout to the branch - let object = repository.revparse_single(&("refs/heads/".to_owned() + branch_name)) - .map_err(|e| format!("Failed to revparse single: {}", e))?; - repository.checkout_tree(&object, None) - .map_err(|e| format!("Failed to checkout tree: {}", e))?; - repository.set_head(&format!("refs/heads/{}", branch_name)) - .map_err(|e| format!("Failed to set head: {}", e))?; +// // Checkout to the branch +// let object = repository.revparse_single(&("refs/heads/".to_owned() + branch_name)) +// .map_err(|e| format!("Failed to revparse single: {}", e))?; +// repository.checkout_tree(&object, None) +// .map_err(|e| format!("Failed to checkout tree: {}", e))?; +// repository.set_head(&format!("refs/heads/{}", branch_name)) +// .map_err(|e| format!("Failed to set head: {}", e))?; - Ok(branch) -} +// Ok(branch) +// } -/// Similar to git add . -pub fn stage_all_changes(repository: &Repository) -> Result<(), String> { +pub fn stage_changes(repository: &Repository, file_changes: &Vec) -> Result<(), String> { let mut index = repository.index() .map_err(|e| format!("Failed to get index: {}", e))?; - index.add_all(["*"].iter(), IndexAddOption::DEFAULT, None) - .map_err(|e| format!("Failed to add files to index: {}", e))?; + + for file_change in file_changes { + match file_change.status { + FileChangeStatus::ADDED | FileChangeStatus::MODIFIED => { + index.add_path(std::path::Path::new(&file_change.path)) + .map_err(|e| format!("Failed to add file to index: {}", e))?; + }, + FileChangeStatus::DELETED => { + index.remove_path(std::path::Path::new(&file_change.path)) + .map_err(|e| format!("Failed to remove file from index: {}", e))?; + }, + } + } + index.write() .map_err(|e| format!("Failed to write index: {}", e))?; - Ok(()) + + Ok(()) } -/// Returns: -/// -/// A tuple containing the number of new files, modified files, and deleted files. -pub fn count_file_changes(repository: &Repository, include_unstaged: bool) -> Result<(usize, usize, usize), String> { - let (mut new_files, mut modified_files, mut deleted_files) = (0, 0, 0); +pub fn get_file_changes(repository: &Repository, include_unstaged: bool) -> Result, String> { + let mut result = Vec::new(); let statuses = repository.statuses(None) .map_err(|e| format!("Failed to get statuses: {}", e))?; for entry in statuses.iter() { let status = entry.status(); - if status.contains(Status::INDEX_NEW) { new_files += 1; } - if status.contains(Status::INDEX_MODIFIED) { modified_files += 1;} - if status.contains(Status::INDEX_DELETED) { deleted_files += 1; } + if status.contains(Status::INDEX_NEW) { + result.push(FileChange {status: FileChangeStatus::ADDED, path: entry.path().unwrap().to_string()}) + } + if status.contains(Status::INDEX_MODIFIED) { + result.push(FileChange {status: FileChangeStatus::MODIFIED, path: entry.path().unwrap().to_string()}) + } + if status.contains(Status::INDEX_DELETED) { + result.push(FileChange {status: FileChangeStatus::DELETED, path: entry.path().unwrap().to_string()}) + } if include_unstaged { - if status.contains(Status::WT_NEW) { new_files += 1; } - if status.contains(Status::WT_MODIFIED) { modified_files += 1;} - if status.contains(Status::WT_DELETED) { deleted_files += 1; } + if status.contains(Status::WT_NEW) { + result.push(FileChange {status: FileChangeStatus::ADDED, path: entry.path().unwrap().to_string()}) + } + if status.contains(Status::WT_MODIFIED) { + result.push(FileChange {status: FileChangeStatus::MODIFIED, path: entry.path().unwrap().to_string()}) + } + if status.contains(Status::WT_DELETED) { + result.push(FileChange {status: FileChangeStatus::DELETED, path: entry.path().unwrap().to_string()}) + } } } - Ok((new_files, modified_files, deleted_files)) + Ok(result) +} + +pub fn get_configured_author_email_and_name(repository: &Repository) -> Result<(String, String), String> { + let config = repository.config().map_err(|e| format!("Failed to get repository config: {}", e))?; + let author_email = config.get_string("user.email") + .map_err(|e| format!("Failed to get author email: {}", e))?; + let author_name = config.get_string("user.name") + .map_err(|e| format!("Failed to get author name: {}", e))?; + Ok((author_email, author_name)) } pub fn commit(repository: &Repository, branch: &Branch, message: &str, author_name: &str, author_email: &str) -> Result { @@ -104,7 +170,7 @@ pub fn commit(repository: &Repository, branch: &Branch, message: &str, author_na repository.find_commit(target) .map_err(|e| format!("Failed to find branch commit: {}", e))? } else { - return Err("No parent commits found (initial commit is not supported)".to_string()); + return Err("No parent commits found".to_string()); }; repository.commit( @@ -112,16 +178,26 @@ pub fn commit(repository: &Repository, branch: &Branch, message: &str, author_na ).map_err(|e| format!("Failed to create commit: {}", e)) } -/// Similar to `git diff`, but including untracked files. -pub fn git_diff_from_all_changes(repository: &Repository) -> Result { +/// Similar to `git diff`, from specified file changes. +pub fn git_diff(repository: &Repository, file_changes: &Vec, max_size: usize) -> Result { let mut diff_options = DiffOptions::new(); diff_options.include_untracked(true); diff_options.recurse_untracked_dirs(true); + for file_change in file_changes { + diff_options.pathspec(&file_change.path); + } + + let mut sorted_file_changes = file_changes.clone(); + sorted_file_changes.sort_by_key(|fc| { + std::fs::metadata(&fc.path).map(|meta| meta.len()).unwrap_or(0) + }); // Create a new temporary tree, with all changes staged let mut index = repository.index().map_err(|e| format!("Failed to get repository index: {}", e))?; - index.add_all(["*"].iter(), IndexAddOption::DEFAULT, None) - .map_err(|e| format!("Failed to add files to index: {}", e))?; + for file_change in &sorted_file_changes { + index.add_path(std::path::Path::new(&file_change.path)) + .map_err(|e| format!("Failed to add file to index: {}", e))?; + } let oid = index.write_tree().map_err(|e| format!("Failed to write tree: {}", e))?; let new_tree = repository.find_tree(oid).map_err(|e| format!("Failed to find tree: {}", e))?; @@ -133,10 +209,54 @@ pub fn git_diff_from_all_changes(repository: &Repository) -> Result max_size { + diff_str.truncate(max_size - 4); + diff_str.push_str("...\n"); + } + } true }).map_err(|e| format!("Failed to print diff: {}", e))?; Ok(diff_str) } + +pub async fn get_commit_information_from_current_changes(gcx: Arc>) -> Vec { + const MAX_DIFF_SIZE: usize = 4096; + let mut commits = Vec::new(); + + for project_path in crate::files_correction::get_project_dirs(gcx.clone()).await { + let repository = match git2::Repository::open(&project_path) { + Ok(repo) => repo, + Err(e) => { error!("{}", e); continue; } + }; + + let file_changes = match crate::git::get_file_changes(&repository, true) { + Ok(changes) if changes.is_empty() => { continue; } + Ok(changes) => changes, + Err(e) => { error!("{}", e); continue; } + }; + + let diff = match git_diff(&repository, &file_changes, MAX_DIFF_SIZE) { + Ok(d) if d.is_empty() => { continue; } + Ok(d) => d, + Err(e) => { error!("{}", e); continue; } + }; + + let commit_msg = match generate_commit_message_by_diff(gcx.clone(), &diff, &None).await { + Ok(msg) => msg, + Err(e) => { error!("{}", e); continue; } + }; + + commits.push(CommitInfo { + project_path: Url::from_file_path(&project_path).ok().unwrap_or_else(|| Url::parse("file:///").unwrap()), + commit_message: commit_msg, + file_changes, + }); + } + + commits +} diff --git a/src/http/routers/v1.rs b/src/http/routers/v1.rs index d754ed8c2..8784e76be 100644 --- a/src/http/routers/v1.rs +++ b/src/http/routers/v1.rs @@ -22,7 +22,7 @@ use crate::http::routers::v1::chat::{handle_v1_chat, handle_v1_chat_completions} use crate::http::routers::v1::chat_based_handlers::handle_v1_commit_message_from_diff; use crate::http::routers::v1::dashboard::get_dashboard_plots; use crate::http::routers::v1::docker::{handle_v1_docker_container_action, handle_v1_docker_container_list}; -use crate::http::routers::v1::git::handle_v1_git_stage_and_commit; +use crate::http::routers::v1::git::handle_v1_git_commit; use crate::http::routers::v1::graceful_shutdown::handle_v1_graceful_shutdown; use crate::http::routers::v1::snippet_accepted::handle_v1_snippet_accepted; use crate::http::routers::v1::telemetry_network::handle_v1_telemetry_network; @@ -112,7 +112,7 @@ pub fn make_v1_router() -> Router { .route("/sync-files-extract-tar", telemetry_post!(handle_v1_sync_files_extract_tar)) - .route("/git-stage-and-commit", telemetry_post!(handle_v1_git_stage_and_commit)) + .route("/git-commit", telemetry_post!(handle_v1_git_commit)) .route("/system-prompt", telemetry_post!(handle_v1_system_prompt)) // because it works remotely diff --git a/src/http/routers/v1/git.rs b/src/http/routers/v1/git.rs index aae94f1b7..496e69b3e 100644 --- a/src/http/routers/v1/git.rs +++ b/src/http/routers/v1/git.rs @@ -8,57 +8,88 @@ use tokio::sync::RwLock as ARwLock; use url::Url; use crate::custom_error::ScratchError; -use crate::git::{commit, count_file_changes, create_or_checkout_to_branch, stage_all_changes}; +use crate::git::{CommitInfo, stage_changes, get_configured_author_email_and_name}; use crate::global_context::GlobalContext; -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct GitStageAndCommitPost { - chat_id: String, - repository_path: Url, +#[derive(Serialize, Deserialize, Debug)] +pub struct GitCommitPost { + pub commits: Vec, } -pub async fn handle_v1_git_stage_and_commit( +#[derive(Serialize, Deserialize, Debug)] +pub struct GitError { + pub error_message: String, + pub project_name: String, + pub project_path: Url, +} + +pub async fn handle_v1_git_commit( Extension(_gcx): Extension>>, body_bytes: hyper::body::Bytes, ) -> Result, ScratchError> { - let post = serde_json::from_slice::(&body_bytes) + let post = serde_json::from_slice::(&body_bytes) .map_err(|e| ScratchError::new(StatusCode::UNPROCESSABLE_ENTITY, format!("JSON problem: {}", e)))?; - let repo_path = crate::files_correction::canonical_path( - &post.repository_path.to_file_path().unwrap_or_default().to_string_lossy().to_string()); - let repository = Repository::open(&repo_path) - .map_err(|e| ScratchError::new(StatusCode::INTERNAL_SERVER_ERROR, format!("Could not open repository: {}", e)))?; + let mut error_log = Vec::new(); + let mut commits_applied = Vec::new(); + + for commit in post.commits { + let repo_path = crate::files_correction::to_pathbuf_normalize( + &commit.project_path.to_file_path().unwrap_or_default().display().to_string()); + + let project_name = commit.project_path.to_file_path().ok() + .and_then(|path| path.file_name().map(|name| name.to_string_lossy().into_owned())) + .unwrap_or_else(|| "".to_string()); - let branch_name = format!("refact-{}", post.chat_id); - let branch = create_or_checkout_to_branch(&repository, &branch_name) - .map_err(|e| ScratchError::new(StatusCode::INTERNAL_SERVER_ERROR, e))?; + let git_error = |msg: String| -> GitError { + GitError { + error_message: msg, + project_name: project_name.clone(), + project_path: commit.project_path.clone(), + } + }; - stage_all_changes(&repository) - .map_err(|e| ScratchError::new(StatusCode::INTERNAL_SERVER_ERROR, e))?; + let repository = match Repository::open(&repo_path) { + Ok(repo) => repo, + Err(e) => { error_log.push(git_error(format!("Failed to open repo: {}", e))); continue; } + }; - let (new_files, modified_files, deleted_files) = count_file_changes(&repository, false) - .map_err(|e| ScratchError::new(StatusCode::INTERNAL_SERVER_ERROR, e))?; + if let Err(stage_err) = stage_changes(&repository, &commit.file_changes) { + error_log.push(git_error(stage_err)); + continue; + } + + let (author_email, author_name) = match get_configured_author_email_and_name(&repository) { + Ok(email_and_name) => email_and_name, + Err(err) => { + error_log.push(git_error(err)); + continue; + } + }; + + let branch = match repository.head().map(|reference| git2::Branch::wrap(reference)) { + Ok(branch) => branch, + Err(e) => { error_log.push(git_error(format!("Failed to get current branch: {}", e))); continue; } + }; + + let commit_oid = match crate::git::commit(&repository, &branch, &commit.commit_message, &author_name, &author_email) { + Ok(oid) => oid, + Err(e) => { error_log.push(git_error(e)); continue; } + }; - let commit_oid = if new_files + modified_files + deleted_files != 0 { - Some(commit( - &repository, - &branch, - &format!("Refact agent commit in chat {} at {}", post.chat_id, chrono::Utc::now().format("%Y-%m-%d %H:%M:%S")), - "Refact Agent", - "agent@refact.ai", - ).map_err(|e| ScratchError::new(StatusCode::INTERNAL_SERVER_ERROR, e))?) - } else { - None - }; + commits_applied.push(serde_json::json!({ + "project_name": project_name, + "project_path": commit.project_path.to_string(), + "commit_oid": commit_oid.to_string(), + })); + } Ok(Response::builder() .status(StatusCode::OK) .header("Content-Type", "application/json") - .body(Body::from(serde_json::json!({ - "commit_oid": commit_oid.map(|x| x.to_string()), - "new_files": new_files, - "modified_files": modified_files, - "deleted_files": deleted_files, - }).to_string())) + .body(Body::from(serde_json::to_string(&serde_json::json!({ + "commits_applied": commits_applied, + "error_log": error_log, + })).unwrap())) .unwrap()) } \ No newline at end of file diff --git a/src/http/routers/v1/links.rs b/src/http/routers/v1/links.rs index 1308240f0..ef92ecec2 100644 --- a/src/http/routers/v1/links.rs +++ b/src/http/routers/v1/links.rs @@ -3,17 +3,17 @@ use std::fs; use axum::Extension; use axum::http::{Response, StatusCode}; use hyper::Body; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize, Serializer}; use tokio::sync::RwLock as ARwLock; -use tracing::error; -use crate::agentic::generate_commit_message::generate_commit_message_by_diff; use crate::call_validation::{ChatMessage, ChatMeta, ChatMode}; use crate::custom_error::ScratchError; use crate::global_context::GlobalContext; use crate::integrations::go_to_configuration_message; use crate::tools::tool_patch_aux::tickets_parsing::get_tickets_from_messages; use crate::agentic::generate_follow_up_message::generate_follow_up_message; +use crate::git::get_commit_information_from_current_changes; +use crate::http::routers::v1::git::GitCommitPost; #[derive(Deserialize, Clone, Debug)] pub struct LinksPost { @@ -32,7 +32,7 @@ enum LinkAction { SummarizeProject, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Debug)] pub struct Link { // XXX rename: // link_action @@ -44,16 +44,23 @@ pub struct Link { #[serde(skip_serializing_if = "Option::is_none")] goto: Option, #[serde(skip_serializing_if = "Option::is_none")] - // projects: Option>, current_config_file: Option, // XXX rename link_tooltip: String, + link_payload: Option, } -#[derive(Serialize, Deserialize, Debug)] -pub struct ProjectCommit { - path: String, - commit_message: String, +#[derive(Debug)] +pub enum LinkPayload { + CommitPayload(GitCommitPost), } +impl Serialize for LinkPayload { + fn serialize(&self, serializer: S) -> Result { + match self { + LinkPayload::CommitPayload(post) => post.serialize(serializer), + } + } +} + pub async fn handle_v1_links( Extension(gcx): Extension>>, @@ -62,6 +69,7 @@ pub async fn handle_v1_links( let post = serde_json::from_slice::(&body_bytes) .map_err(|e| ScratchError::new(StatusCode::UNPROCESSABLE_ENTITY, format!("JSON problem: {}", e)))?; let mut links = Vec::new(); + let mut uncommited_changes_warning = String::new(); tracing::info!("for links, post.meta.chat_mode == {:?}", post.meta.chat_mode); let (integrations_map, integration_yaml_errors) = crate::integrations::running_integrations::load_integrations(gcx.clone(), "".to_string(), gcx.read().await.cmdline.experimental).await; @@ -75,6 +83,7 @@ pub async fn handle_v1_links( goto: None, current_config_file: summary_path_option, link_tooltip: format!("Project summary is a starting point for Refact Agent."), + link_payload: None, }); } else { // exists @@ -94,6 +103,7 @@ pub async fn handle_v1_links( goto: Some(format!("SETTINGS:{igname}")), current_config_file: None, link_tooltip: format!(""), + link_payload: None, }); } else { tracing::info!("tool {} present => happy", igname); @@ -115,29 +125,62 @@ pub async fn handle_v1_links( } } - if post.meta.chat_mode == ChatMode::CONFIGURE && !get_tickets_from_messages(gcx.clone(), &post.messages).await.is_empty() { + if post.meta.chat_mode == ChatMode::CONFIGURE { links.push(Link { - action: LinkAction::PatchAll, - text: "Save and return".to_string(), + action: LinkAction::Goto, + text: "Return".to_string(), goto: Some("SETTINGS:DEFAULT".to_string()), current_config_file: None, link_tooltip: format!(""), + link_payload: None, }); + + if !get_tickets_from_messages(gcx.clone(), &post.messages).await.is_empty() { + links.push(Link { + action: LinkAction::PatchAll, + text: "Save and return".to_string(), + goto: Some("SETTINGS:DEFAULT".to_string()), + current_config_file: None, + link_tooltip: format!(""), + link_payload: None, + }); + } } - // if post.meta.chat_mode == ChatMode::AGENT { - // let (project_commits, files_changed) = generate_commit_messages_with_current_changes(gcx.clone()).await; - // if !project_commits.is_empty() { - // links.push(Link { - // action: LinkAction::Commit, - // text: format!("Commit {files_changed} files"), - // goto: None, - // // projects: Some(project_commits), - // current_config_file: None, - // link_tooltip: format!(""), - // }); - // } - // } + if post.meta.chat_mode == ChatMode::AGENT { + let mut project_changes = Vec::new(); + for commit in get_commit_information_from_current_changes(gcx.clone()).await { + let project_name = commit.project_path.to_file_path().ok() + .and_then(|path| path.file_name().map(|name| name.to_string_lossy().into_owned())) + .unwrap_or_else(|| "".to_string()); + let tooltip_message = format!( + "git commit -m \"{}{}\"\n{}", + commit.commit_message.lines().next().unwrap_or(""), + if commit.commit_message.lines().count() > 1 { "..." } else { "" }, + commit.file_changes.iter().map(|f| format!("{} {}", f.status.initial(), f.path)).collect::>().join("\n"), + ); + project_changes.push(format!( + "In project {project_name}: {}{}", + commit.file_changes.iter().take(3).map(|f| format!("{} {}", f.status.initial(), f.path)).collect::>().join(", "), + if commit.file_changes.len() > 3 { ", ..." } else { "" }, + )); + links.push(Link { + action: LinkAction::Commit, + text: format!("Commit {} files in `{}`", commit.file_changes.len(), project_name), + goto: Some("LINKS_AGAIN".to_string()), + current_config_file: None, + link_tooltip: tooltip_message, + link_payload: Some(LinkPayload::CommitPayload(GitCommitPost { commits: vec![commit] })), + }); + } + if !project_changes.is_empty() && post.messages.is_empty() { + if project_changes.len() > 4 { + project_changes.truncate(4); + project_changes.push("...".to_string()); + } + uncommited_changes_warning = format!("You have uncommitted changes:\n```\n{}\n```\n⚠️ You might have a problem rolling back agent's changes.", project_changes.join("\n")); + } + } if post.meta.chat_mode == ChatMode::AGENT { for failed_integr_name in failed_integration_names_after_last_user_message(&post.messages) { @@ -147,6 +190,7 @@ pub async fn handle_v1_links( goto: Some(format!("SETTINGS:{failed_integr_name}")), current_config_file: None, link_tooltip: format!(""), + link_payload: None, }) } } @@ -158,6 +202,7 @@ pub async fn handle_v1_links( goto: Some(format!("SETTINGS:{}", e.integr_config_path)), current_config_file: None, link_tooltip: format!("Error at line {}: {}", e.error_line, e.error_msg), + link_payload: None, }); } @@ -173,6 +218,7 @@ pub async fn handle_v1_links( goto: None, current_config_file: None, link_tooltip: format!(""), + link_payload: None, }); } } @@ -182,45 +228,10 @@ pub async fn handle_v1_links( Ok(Response::builder() .status(StatusCode::OK) .header("Content-Type", "application/json") - .body(Body::from(serde_json::to_string_pretty(&serde_json::json!({"links": links})).unwrap())) - .unwrap()) -} - -async fn generate_commit_messages_with_current_changes(gcx: Arc>) -> (Vec, usize) { - let mut project_commits = Vec::new(); - let mut total_file_changes = 0; - - for project_path in crate::files_correction::get_project_dirs(gcx.clone()).await { - let repository = match git2::Repository::open(&project_path) { - Ok(repo) => repo, - Err(e) => { error!("{}", e); continue; } - }; - - let (added, modified, deleted) = match crate::git::count_file_changes(&repository, true) { - Ok((0, 0, 0)) => { continue; } - Ok(changes) => changes, - Err(e) => { error!("{}", e); continue; } - }; - - let diff = match crate::git::git_diff_from_all_changes(&repository) { - Ok(d) if d.is_empty() => { continue; } - Ok(d) => d, - Err(e) => { error!("{}", e); continue; } - }; - - let commit_msg = match generate_commit_message_by_diff(gcx.clone(), &diff, &None).await { - Ok(msg) => msg, - Err(e) => { error!("{}", e); continue; } - }; - - project_commits.push(ProjectCommit { - path: project_path.to_string_lossy().to_string(), - commit_message: commit_msg, - }); - total_file_changes += added + modified + deleted; - } - - (project_commits, total_file_changes) + .body(Body::from(serde_json::to_string_pretty(&serde_json::json!({ + "links": links, + "uncommited_changes_warning": uncommited_changes_warning, + })).unwrap())).unwrap()) } fn failed_integration_names_after_last_user_message(messages: &Vec) -> Vec {