From 4ece0862d66d3ff8c2e1273e707a72c71e604d96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Humberto=20Yusta=20G=C3=B3mez?= Date: Mon, 9 Dec 2024 17:59:28 +0100 Subject: [PATCH 1/8] feat: add return button --- src/http/routers/v1/links.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/http/routers/v1/links.rs b/src/http/routers/v1/links.rs index 1308240f0..2533cc8a1 100644 --- a/src/http/routers/v1/links.rs +++ b/src/http/routers/v1/links.rs @@ -115,14 +115,24 @@ 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!(""), }); + + 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!(""), + }); + } } // if post.meta.chat_mode == ChatMode::AGENT { From 1bdedf604630bae9bc709539bb610121d027d9be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Humberto=20Yusta=20G=C3=B3mez?= Date: Mon, 9 Dec 2024 23:38:29 +0100 Subject: [PATCH 2/8] feat: new format of commit link, one link per project with changes --- src/git.rs | 64 ++++++++++++++++---- src/http/routers/v1.rs | 4 +- src/http/routers/v1/git.rs | 110 +++++++++++++++++------------------ src/http/routers/v1/links.rs | 86 ++++++++++++++++++--------- 4 files changed, 167 insertions(+), 97 deletions(-) diff --git a/src/git.rs b/src/git.rs index fd4167a48..2398a02f3 100644 --- a/src/git.rs +++ b/src/git.rs @@ -1,7 +1,30 @@ use std::path::PathBuf; +use serde::{Serialize, Deserialize}; use tracing::error; use git2::{Branch, BranchType, DiffOptions, IndexAddOption, Oid, Repository, Signature, Status, StatusOptions}; +#[derive(Serialize, Deserialize, Debug)] +pub struct FileChange { + pub path: String, + pub status: FileChangeStatus, +} + +#[derive(Serialize, Deserialize, Debug)] +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) .map_err(|e| error!("Failed to open repository: {}", e)).ok()?; @@ -65,24 +88,36 @@ pub fn stage_all_changes(repository: &Repository) -> Result<(), String> { /// 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 commit(repository: &Repository, branch: &Branch, message: &str, author_name: &str, author_email: &str) -> Result { @@ -113,15 +148,20 @@ pub fn commit(repository: &Repository, branch: &Branch, message: &str, author_na } /// Similar to `git diff`, but including untracked files. -pub fn git_diff_from_all_changes(repository: &Repository) -> Result { +pub fn git_diff(repository: &Repository, file_changes: &Vec) -> 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); + } // 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 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))?; diff --git a/src/http/routers/v1.rs b/src/http/routers/v1.rs index d754ed8c2..09fd3a42e 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_stage_and_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-stage-and-commit", telemetry_post!(handle_v1_git_stage_and_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..5adfb5a26 100644 --- a/src/http/routers/v1/git.rs +++ b/src/http/routers/v1/git.rs @@ -1,64 +1,64 @@ -use std::sync::Arc; -use axum::Extension; -use axum::http::{Response, StatusCode}; -use git2::Repository; -use hyper::Body; -use serde::{Deserialize, Serialize}; -use tokio::sync::RwLock as ARwLock; -use url::Url; +// use std::sync::Arc; +// use axum::Extension; +// use axum::http::{Response, StatusCode}; +// use git2::Repository; +// use hyper::Body; +// use serde::{Deserialize, Serialize}; +// 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::global_context::GlobalContext; +// use crate::custom_error::ScratchError; +// use crate::git::{commit, create_or_checkout_to_branch, stage_all_changes}; +// use crate::global_context::GlobalContext; -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct GitStageAndCommitPost { - chat_id: String, - repository_path: Url, -} +// #[derive(Serialize, Deserialize, Clone, Debug)] +// pub struct GitStageAndCommitPost { +// chat_id: String, +// repository_path: Url, +// } -pub async fn handle_v1_git_stage_and_commit( - Extension(_gcx): Extension>>, - body_bytes: hyper::body::Bytes, -) -> Result, ScratchError> { - let post = serde_json::from_slice::(&body_bytes) - .map_err(|e| ScratchError::new(StatusCode::UNPROCESSABLE_ENTITY, format!("JSON problem: {}", e)))?; +// pub async fn handle_v1_git_stage_and_commit( +// Extension(_gcx): Extension>>, +// body_bytes: hyper::body::Bytes, +// ) -> Result, ScratchError> { +// 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 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 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 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))?; - stage_all_changes(&repository) - .map_err(|e| ScratchError::new(StatusCode::INTERNAL_SERVER_ERROR, e))?; +// stage_all_changes(&repository) +// .map_err(|e| ScratchError::new(StatusCode::INTERNAL_SERVER_ERROR, e))?; - let (new_files, modified_files, deleted_files) = count_file_changes(&repository, false) - .map_err(|e| ScratchError::new(StatusCode::INTERNAL_SERVER_ERROR, e))?; +// let (new_files, modified_files, deleted_files) = count_file_changes(&repository, false) +// .map_err(|e| ScratchError::new(StatusCode::INTERNAL_SERVER_ERROR, e))?; - 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 - }; +// 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 +// }; - 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())) - .unwrap()) -} \ No newline at end of file +// 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())) +// .unwrap()) +// } \ No newline at end of file diff --git a/src/http/routers/v1/links.rs b/src/http/routers/v1/links.rs index 2533cc8a1..b2c20c334 100644 --- a/src/http/routers/v1/links.rs +++ b/src/http/routers/v1/links.rs @@ -3,9 +3,10 @@ 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 url::Url; use crate::agentic::generate_commit_message::generate_commit_message_by_diff; use crate::call_validation::{ChatMessage, ChatMeta, ChatMode}; @@ -14,6 +15,7 @@ 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::FileChange; #[derive(Deserialize, Clone, Debug)] pub struct LinksPost { @@ -32,7 +34,7 @@ enum LinkAction { SummarizeProject, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Debug)] pub struct Link { // XXX rename: // link_action @@ -44,15 +46,28 @@ 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(Debug)] +pub enum LinkPayload { + CommitPayload(CommitInfo), +} +impl Serialize for LinkPayload { + fn serialize(&self, serializer: S) -> Result { + match self { + LinkPayload::CommitPayload(commit_payload) => commit_payload.serialize(serializer), + } + } } #[derive(Serialize, Deserialize, Debug)] -pub struct ProjectCommit { - path: String, +pub struct CommitInfo { + project_path: Url, commit_message: String, + file_changes: Vec, } pub async fn handle_v1_links( @@ -75,6 +90,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 +110,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); @@ -122,6 +139,7 @@ pub async fn handle_v1_links( 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() { @@ -131,23 +149,32 @@ pub async fn handle_v1_links( 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 { + 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 commmit -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"), + ); + 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(commit)), + }); + } + } if post.meta.chat_mode == ChatMode::AGENT { for failed_integr_name in failed_integration_names_after_last_user_message(&post.messages) { @@ -157,6 +184,7 @@ pub async fn handle_v1_links( goto: Some(format!("SETTINGS:{failed_integr_name}")), current_config_file: None, link_tooltip: format!(""), + link_payload: None, }) } } @@ -168,6 +196,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, }); } @@ -183,6 +212,7 @@ pub async fn handle_v1_links( goto: None, current_config_file: None, link_tooltip: format!(""), + link_payload: None, }); } } @@ -196,23 +226,23 @@ pub async fn handle_v1_links( .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; +async fn get_commit_information_from_current_changes(gcx: Arc>) -> Vec { + 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; } }; + tracing::info!("repository opened"); - let (added, modified, deleted) = match crate::git::count_file_changes(&repository, true) { - Ok((0, 0, 0)) => { 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 crate::git::git_diff_from_all_changes(&repository) { + let diff = match crate::git::git_diff(&repository, &file_changes) { Ok(d) if d.is_empty() => { continue; } Ok(d) => d, Err(e) => { error!("{}", e); continue; } @@ -223,14 +253,14 @@ async fn generate_commit_messages_with_current_changes(gcx: Arc { error!("{}", e); continue; } }; - project_commits.push(ProjectCommit { - path: project_path.to_string_lossy().to_string(), + 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, }); - total_file_changes += added + modified + deleted; } - (project_commits, total_file_changes) + commits } fn failed_integration_names_after_last_user_message(messages: &Vec) -> Vec { From 2d01da55421eb352ababe2dca3e8c454fb08dc83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Humberto=20Yusta=20G=C3=B3mez?= Date: Tue, 10 Dec 2024 11:30:00 +0100 Subject: [PATCH 3/8] feat: stage changes based on file changes and get configured author email and name --- src/git.rs | 86 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 52 insertions(+), 34 deletions(-) diff --git a/src/git.rs b/src/git.rs index 2398a02f3..af52a84ab 100644 --- a/src/git.rs +++ b/src/git.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; 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}; #[derive(Serialize, Deserialize, Debug)] pub struct FileChange { @@ -51,43 +51,52 @@ 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))? - } - }; - - // 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) -} - -/// Similar to git add . -pub fn stage_all_changes(repository: &Repository) -> Result<(), String> { +// 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))?; + +// Ok(branch) +// } + +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 get_file_changes(repository: &Repository, include_unstaged: bool) -> Result, String> { let mut result = Vec::new(); @@ -120,6 +129,15 @@ pub fn get_file_changes(repository: &Repository, include_unstaged: bool) -> Resu 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 { let mut index = repository.index() @@ -139,7 +157,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( @@ -147,7 +165,7 @@ 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. +/// Similar to `git diff`, from specified file changes. pub fn git_diff(repository: &Repository, file_changes: &Vec) -> Result { let mut diff_options = DiffOptions::new(); diff_options.include_untracked(true); From bf7c8be855d6257a22dc36b127225d0621ac61dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Humberto=20Yusta=20G=C3=B3mez?= Date: Tue, 10 Dec 2024 11:30:44 +0100 Subject: [PATCH 4/8] feat: add commit handler --- src/http/routers/v1.rs | 4 +- src/http/routers/v1/git.rs | 148 ++++++++++++++++++++++------------- src/http/routers/v1/links.rs | 14 +--- 3 files changed, 99 insertions(+), 67 deletions(-) diff --git a/src/http/routers/v1.rs b/src/http/routers/v1.rs index 09fd3a42e..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 5adfb5a26..f888e28a3 100644 --- a/src/http/routers/v1/git.rs +++ b/src/http/routers/v1/git.rs @@ -1,64 +1,102 @@ -// use std::sync::Arc; -// use axum::Extension; -// use axum::http::{Response, StatusCode}; -// use git2::Repository; -// use hyper::Body; -// use serde::{Deserialize, Serialize}; -// use tokio::sync::RwLock as ARwLock; -// use url::Url; +use std::sync::Arc; +use axum::Extension; +use axum::http::{Response, StatusCode}; +use git2::Repository; +use hyper::Body; +use serde::{Deserialize, Serialize}; +use tokio::sync::RwLock as ARwLock; +use url::Url; -// use crate::custom_error::ScratchError; -// use crate::git::{commit, create_or_checkout_to_branch, stage_all_changes}; -// use crate::global_context::GlobalContext; +use crate::custom_error::ScratchError; +use crate::git::{FileChange, 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( -// Extension(_gcx): Extension>>, -// body_bytes: hyper::body::Bytes, -// ) -> Result, ScratchError> { -// let post = serde_json::from_slice::(&body_bytes) -// .map_err(|e| ScratchError::new(StatusCode::UNPROCESSABLE_ENTITY, format!("JSON problem: {}", e)))?; +#[derive(Serialize, Deserialize, Debug)] +pub struct CommitInfo { + pub project_path: Url, + pub commit_message: String, + pub file_changes: Vec, +} -// 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)))?; +#[derive(Serialize, Deserialize, Debug)] +pub struct GitError { + pub error_message: String, + pub project_name: String, + pub project_path: Url, +} -// 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))?; +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) + .map_err(|e| ScratchError::new(StatusCode::UNPROCESSABLE_ENTITY, format!("JSON problem: {}", e)))?; -// stage_all_changes(&repository) -// .map_err(|e| ScratchError::new(StatusCode::INTERNAL_SERVER_ERROR, e))?; + let mut error_log = Vec::new(); + let mut commits_applied = Vec::new(); -// let (new_files, modified_files, deleted_files) = count_file_changes(&repository, false) -// .map_err(|e| ScratchError::new(StatusCode::INTERNAL_SERVER_ERROR, e))?; + 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 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 -// }; + 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 git_error = |msg: String| -> GitError { + GitError { + error_message: msg, + project_name: project_name.clone(), + project_path: commit.project_path.clone(), + } + }; + + let repository = match Repository::open(&repo_path) { + Ok(repo) => repo, + Err(e) => { error_log.push(git_error(format!("Failed to open repo: {}", e))); continue; } + }; + + 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; } + }; + + 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())) -// .unwrap()) -// } \ No newline at end of file + Ok(Response::builder() + .status(StatusCode::OK) + .header("Content-Type", "application/json") + .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 b2c20c334..665be55d9 100644 --- a/src/http/routers/v1/links.rs +++ b/src/http/routers/v1/links.rs @@ -15,7 +15,7 @@ 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::FileChange; +use crate::http::routers::v1::git::{CommitInfo, GitCommitPost}; #[derive(Deserialize, Clone, Debug)] pub struct LinksPost { @@ -53,22 +53,16 @@ pub struct Link { #[derive(Debug)] pub enum LinkPayload { - CommitPayload(CommitInfo), + CommitPayload(GitCommitPost), } impl Serialize for LinkPayload { fn serialize(&self, serializer: S) -> Result { match self { - LinkPayload::CommitPayload(commit_payload) => commit_payload.serialize(serializer), + LinkPayload::CommitPayload(post) => post.serialize(serializer), } } } -#[derive(Serialize, Deserialize, Debug)] -pub struct CommitInfo { - project_path: Url, - commit_message: String, - file_changes: Vec, -} pub async fn handle_v1_links( Extension(gcx): Extension>>, @@ -171,7 +165,7 @@ pub async fn handle_v1_links( goto: Some("LINKS_AGAIN".to_string()), current_config_file: None, link_tooltip: tooltip_message, - link_payload: Some(LinkPayload::CommitPayload(commit)), + link_payload: Some(LinkPayload::CommitPayload(GitCommitPost { commits: vec![commit] })), }); } } From 06a84f8323d76a7db24960650f9ad4a360219eac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Humberto=20Yusta=20G=C3=B3mez?= Date: Tue, 10 Dec 2024 12:35:43 +0100 Subject: [PATCH 5/8] feat: add uncommited warning message --- src/http/routers/v1/links.rs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/http/routers/v1/links.rs b/src/http/routers/v1/links.rs index 665be55d9..874a0eaeb 100644 --- a/src/http/routers/v1/links.rs +++ b/src/http/routers/v1/links.rs @@ -71,6 +71,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; @@ -149,6 +150,7 @@ pub async fn handle_v1_links( } 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())) @@ -159,6 +161,11 @@ pub async fn handle_v1_links( 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), @@ -168,6 +175,13 @@ pub async fn handle_v1_links( link_payload: Some(LinkPayload::CommitPayload(GitCommitPost { commits: vec![commit] })), }); } + if !project_changes.is_empty() { + if project_changes.len() > 4 { + project_changes.truncate(4); + project_changes.push("...".to_string()); + } + uncommited_changes_warning = format!("You have uncommitted changes, which may cause issues when rolling back agent changes:\n{}", project_changes.join("\n")); + } } if post.meta.chat_mode == ChatMode::AGENT { @@ -216,8 +230,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()) + .body(Body::from(serde_json::to_string_pretty(&serde_json::json!({ + "links": links, + "uncommited_changes_warning": uncommited_changes_warning, + })).unwrap())).unwrap()) } async fn get_commit_information_from_current_changes(gcx: Arc>) -> Vec { @@ -228,7 +244,6 @@ async fn get_commit_information_from_current_changes(gcx: Arc repo, Err(e) => { error!("{}", e); continue; } }; - tracing::info!("repository opened"); let file_changes = match crate::git::get_file_changes(&repository, true) { Ok(changes) if changes.is_empty() => { continue; } From da6fee6f242e6eca124286e1286792bf7e5d11e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Humberto=20Yusta=20G=C3=B3mez?= Date: Tue, 10 Dec 2024 13:34:04 +0100 Subject: [PATCH 6/8] fix: limit git diff size + move git commit info code to src/git.rs --- src/git.rs | 72 +++++++++++++++++++++++++++++++++--- src/http/routers/v1/git.rs | 9 +---- src/http/routers/v1/links.rs | 42 +-------------------- 3 files changed, 70 insertions(+), 53 deletions(-) diff --git a/src/git.rs b/src/git.rs index af52a84ab..b6b98a7ac 100644 --- a/src/git.rs +++ b/src/git.rs @@ -1,15 +1,28 @@ +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, 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)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub enum FileChangeStatus { ADDED, MODIFIED, @@ -166,7 +179,7 @@ pub fn commit(repository: &Repository, branch: &Branch, message: &str, author_na } /// Similar to `git diff`, from specified file changes. -pub fn git_diff(repository: &Repository, file_changes: &Vec) -> Result { +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); @@ -174,9 +187,14 @@ pub fn git_diff(repository: &Repository, file_changes: &Vec) -> Resu 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))?; - for file_change in file_changes { + 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))?; } @@ -191,10 +209,54 @@ pub fn git_diff(repository: &Repository, file_changes: &Vec) -> Resu let mut diff_str = String::new(); diff.print(git2::DiffFormat::Patch, |_, _, line| { - diff_str.push(line.origin()); - diff_str.push_str(std::str::from_utf8(line.content()).unwrap_or("")); + let line_content = std::str::from_utf8(line.content()).unwrap_or(""); + if diff_str.len() + line_content.len() < max_size { + diff_str.push(line.origin()); + diff_str.push_str(line_content); + if diff_str.len() > 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/git.rs b/src/http/routers/v1/git.rs index f888e28a3..496e69b3e 100644 --- a/src/http/routers/v1/git.rs +++ b/src/http/routers/v1/git.rs @@ -8,7 +8,7 @@ use tokio::sync::RwLock as ARwLock; use url::Url; use crate::custom_error::ScratchError; -use crate::git::{FileChange, stage_changes, get_configured_author_email_and_name}; +use crate::git::{CommitInfo, stage_changes, get_configured_author_email_and_name}; use crate::global_context::GlobalContext; #[derive(Serialize, Deserialize, Debug)] @@ -16,13 +16,6 @@ pub struct GitCommitPost { pub commits: Vec, } -#[derive(Serialize, Deserialize, Debug)] -pub struct CommitInfo { - pub project_path: Url, - pub commit_message: String, - pub file_changes: Vec, -} - #[derive(Serialize, Deserialize, Debug)] pub struct GitError { pub error_message: String, diff --git a/src/http/routers/v1/links.rs b/src/http/routers/v1/links.rs index 874a0eaeb..4f3576456 100644 --- a/src/http/routers/v1/links.rs +++ b/src/http/routers/v1/links.rs @@ -5,17 +5,15 @@ use axum::http::{Response, StatusCode}; use hyper::Body; use serde::{Deserialize, Serialize, Serializer}; use tokio::sync::RwLock as ARwLock; -use tracing::error; -use url::Url; -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::http::routers::v1::git::{CommitInfo, GitCommitPost}; +use crate::git::get_commit_information_from_current_changes; +use crate::http::routers::v1::git::GitCommitPost; #[derive(Deserialize, Clone, Debug)] pub struct LinksPost { @@ -236,42 +234,6 @@ pub async fn handle_v1_links( })).unwrap())).unwrap()) } -async fn get_commit_information_from_current_changes(gcx: Arc>) -> Vec { - 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 crate::git::git_diff(&repository, &file_changes) { - 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 -} - fn failed_integration_names_after_last_user_message(messages: &Vec) -> Vec { let last_user_msg_index = messages.iter().rposition(|m| m.role == "user").unwrap_or(0); let tool_calls = messages[last_user_msg_index..].iter().filter(|m| m.role == "assistant") From d14e3d035ec1c3156dd181a785e2c9d70f6c9d56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Humberto=20Yusta=20G=C3=B3mez?= Date: Tue, 10 Dec 2024 19:26:30 +0100 Subject: [PATCH 7/8] fix: extra m in 'commit' and no uncommited warning if there are messages --- src/http/routers/v1/links.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/http/routers/v1/links.rs b/src/http/routers/v1/links.rs index 4f3576456..647fa4036 100644 --- a/src/http/routers/v1/links.rs +++ b/src/http/routers/v1/links.rs @@ -154,7 +154,7 @@ pub async fn handle_v1_links( .and_then(|path| path.file_name().map(|name| name.to_string_lossy().into_owned())) .unwrap_or_else(|| "".to_string()); let tooltip_message = format!( - "git commmit -m \"{}{}\"\n{}", + "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"), @@ -173,7 +173,7 @@ pub async fn handle_v1_links( link_payload: Some(LinkPayload::CommitPayload(GitCommitPost { commits: vec![commit] })), }); } - if !project_changes.is_empty() { + if !project_changes.is_empty() && post.messages.is_empty() { if project_changes.len() > 4 { project_changes.truncate(4); project_changes.push("...".to_string()); From 5b68f8b9f1f0c8fcdb8e0ccf1e58bd844ae72af8 Mon Sep 17 00:00:00 2001 From: Oleg Klimov Date: Wed, 11 Dec 2024 05:23:32 +0100 Subject: [PATCH 8/8] uncommited_changes_warning wording --- src/http/routers/v1/links.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/http/routers/v1/links.rs b/src/http/routers/v1/links.rs index 647fa4036..ef92ecec2 100644 --- a/src/http/routers/v1/links.rs +++ b/src/http/routers/v1/links.rs @@ -134,7 +134,7 @@ pub async fn handle_v1_links( link_tooltip: format!(""), link_payload: None, }); - + if !get_tickets_from_messages(gcx.clone(), &post.messages).await.is_empty() { links.push(Link { action: LinkAction::PatchAll, @@ -154,8 +154,8 @@ pub async fn handle_v1_links( .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(""), + "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"), ); @@ -178,7 +178,7 @@ pub async fn handle_v1_links( project_changes.truncate(4); project_changes.push("...".to_string()); } - uncommited_changes_warning = format!("You have uncommitted changes, which may cause issues when rolling back agent changes:\n{}", project_changes.join("\n")); + 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")); } } @@ -229,7 +229,7 @@ pub async fn handle_v1_links( .status(StatusCode::OK) .header("Content-Type", "application/json") .body(Body::from(serde_json::to_string_pretty(&serde_json::json!({ - "links": links, + "links": links, "uncommited_changes_warning": uncommited_changes_warning, })).unwrap())).unwrap()) }