Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions refact-agent/engine/src/call_validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,14 @@ impl ChatMode {
ChatMode::THINKING_AGENT => true,
}
}

pub fn is_agentic(self) -> bool {
match self {
ChatMode::AGENT | ChatMode::THINKING_AGENT => true,
ChatMode::NO_TOOLS | ChatMode::EXPLORE | ChatMode::CONFIGURE |
ChatMode::PROJECT_SUMMARY => false,
}
}
}

impl Default for ChatMode {
Expand Down
8 changes: 4 additions & 4 deletions refact-agent/engine/src/git/checkpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::custom_error::MapErrToString;
use crate::files_blocklist::reload_indexing_everywhere_if_needed;
use crate::files_correction::{deserialize_path, get_active_workspace_folder, get_project_dirs, serialize_path};
use crate::global_context::GlobalContext;
use crate::git::{FileChange, FileChangeStatus, DiffStatusType, from_unix_glob_pattern_to_gitignore};
use crate::git::{FileChange, FileChangeStatus, from_unix_glob_pattern_to_gitignore};
use crate::git::operations::{checkout_head_and_branch_to_commit, commit, get_commit_datetime, get_diff_statuses, get_diff_statuses_index_to_commit, get_or_create_branch, stage_changes, open_or_init_repo};

#[derive(Default, Serialize, Deserialize, Clone, Debug)]
Expand Down Expand Up @@ -88,7 +88,7 @@ fn get_file_changes_from_nested_repos<'a>(
let mut file_changes_flatened = Vec::new();

for nested_repo in nested_repos {
let nested_repo_changes = get_diff_statuses(DiffStatusType::WorkdirToIndex, nested_repo, include_abs_paths)?;
let (_, nested_repo_changes) = get_diff_statuses(git2::StatusShow::Workdir, nested_repo, include_abs_paths)?;
let nested_repo_workdir = nested_repo.workdir()
.ok_or("Failed to get nested repo workdir".to_string())?;
let nested_repo_rel_path = nested_repo_workdir.strip_prefix(repo_workdir).map_err_to_string()?;
Expand Down Expand Up @@ -132,7 +132,7 @@ pub async fn create_workspace_checkpoint(
let checkpoint = {
let branch = get_or_create_branch(&repo, &format!("refact-{chat_id}"))?;

let mut file_changes = get_diff_statuses(DiffStatusType::WorkdirToIndex, &repo, false)?;
let (_, mut file_changes) = get_diff_statuses(git2::StatusShow::Workdir, &repo, false)?;

let (nested_file_changes, flatened_nested_file_changes) =
get_file_changes_from_nested_repos(&repo, &nested_repos, false)?;
Expand Down Expand Up @@ -234,7 +234,7 @@ pub async fn init_shadow_repos_if_needed(gcx: Arc<ARwLock<GlobalContext>>) -> ()
let t0 = Instant::now();

let initial_commit_result: Result<Oid, String> = (|| {
let mut file_changes = get_diff_statuses(DiffStatusType::WorkdirToIndex, &repo, false)?;
let (_, mut file_changes) = get_diff_statuses(git2::StatusShow::Workdir, &repo, false)?;
let (nested_file_changes, all_nested_changes) =
get_file_changes_from_nested_repos(&repo, &nested_repos, false)?;
file_changes.extend(all_nested_changes);
Expand Down
22 changes: 10 additions & 12 deletions refact-agent/engine/src/git/commit_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use tracing::{error, info, warn};
use crate::global_context::GlobalContext;
use crate::agentic::generate_commit_message::generate_commit_message_by_diff;
use crate::git::CommitInfo;
use crate::git::operations::{get_diff_statuses_workdir_to_head, git_diff_as_string};
use crate::git::operations::{get_diff_statuses, git_diff_head_to_workdir_as_string};

pub async fn get_commit_information_from_current_changes(gcx: Arc<ARwLock<GlobalContext>>) -> Vec<CommitInfo>
{
Expand All @@ -22,16 +22,18 @@ pub async fn get_commit_information_from_current_changes(gcx: Arc<ARwLock<Global
Err(e) => { warn!("{}", e); continue; }
};

let file_changes = match get_diff_statuses_workdir_to_head(&repository) {
Ok(changes) if changes.is_empty() => { continue; }
let (staged_changes, unstaged_changes) = match get_diff_statuses(git2::StatusShow::IndexAndWorkdir, &repository, true) {
Ok((staged, unstaged))
if staged.is_empty() && unstaged.is_empty() => { continue; }
Ok(changes) => changes,
Err(e) => { warn!("{}", e); continue; }
};

commits.push(CommitInfo {
project_path: Url::from_file_path(project_path).ok().unwrap_or_else(|| Url::parse("file:///").unwrap()),
commit_message: "".to_string(),
file_changes,
staged_changes,
unstaged_changes,
});
}

Expand All @@ -41,30 +43,26 @@ pub async fn get_commit_information_from_current_changes(gcx: Arc<ARwLock<Global
pub async fn generate_commit_messages(gcx: Arc<ARwLock<GlobalContext>>, commits: Vec<CommitInfo>) -> Vec<CommitInfo> {
const MAX_DIFF_SIZE: usize = 4096;
let mut commits_with_messages = Vec::new();
for commit in commits {
for mut commit in commits {
let project_path = commit.project_path.to_file_path().ok().unwrap_or_default();

let repository = match git2::Repository::open(&project_path) {
Ok(repo) => repo,
Err(e) => { error!("{}", e); continue; }
};

let diff = match git_diff_as_string(&repository, &commit.file_changes, MAX_DIFF_SIZE) {
let diff = match git_diff_head_to_workdir_as_string(&repository, 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 {
commit.commit_message = match generate_commit_message_by_diff(gcx.clone(), &diff, &None).await {
Ok(msg) => msg,
Err(e) => { error!("{}", e); continue; }
};

commits_with_messages.push(CommitInfo {
project_path: commit.project_path,
commit_message: commit_msg,
file_changes: commit.file_changes,
});
commits_with_messages.push(commit);
}

commits_with_messages
Expand Down
8 changes: 2 additions & 6 deletions refact-agent/engine/src/git/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ use crate::files_correction::{serialize_path, deserialize_path};
pub struct CommitInfo {
pub project_path: url::Url,
pub commit_message: String,
pub file_changes: Vec<FileChange>,
pub staged_changes: Vec<FileChange>,
pub unstaged_changes: Vec<FileChange>,
}

impl CommitInfo {
Expand Down Expand Up @@ -48,11 +49,6 @@ impl FileChangeStatus {
}
}

#[derive(Debug, Copy, Clone)]
pub enum DiffStatusType {
IndexToHead,
WorkdirToIndex,
}

/// It's not equivalent or good match, just best effort so that it works in most cases.
/// Making a 1-to-1 mapping would be very hard.
Expand Down
172 changes: 41 additions & 131 deletions refact-agent/engine/src/git/operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use tracing::error;

use crate::custom_error::MapErrToString;
use crate::files_correction::canonical_path;
use crate::git::{FileChange, FileChangeStatus, DiffStatusType};
use crate::git::{FileChange, FileChangeStatus};

fn status_options(include_unmodified: bool, show: git2::StatusShow) -> git2::StatusOptions {
let mut options = git2::StatusOptions::new();
Expand Down Expand Up @@ -68,19 +68,19 @@ fn is_changed_in_index(status: git2::Status) -> bool {
git2::Status::INDEX_TYPECHANGE)
}

/// Returns (staged_changes, unstaged_changes), note that one of them may be always empty based on show_opt
///
/// If include_abs_path is true, they are included in the FileChanges result, use it if they need to be
/// returned to the client or the absolute paths are needed
pub fn get_diff_statuses(diff_status_type: DiffStatusType, repo: &Repository, include_abs_paths: bool) -> Result<Vec<FileChange>, String> {
pub fn get_diff_statuses(show_opt: git2::StatusShow, repo: &Repository, include_abs_paths: bool) -> Result<(Vec<FileChange>, Vec<FileChange>), String> {
let repo_workdir = repo.workdir()
.ok_or("Failed to get workdir from repository".to_string())?;

let mut result = Vec::new();
let show_opt = match diff_status_type {
DiffStatusType::IndexToHead => git2::StatusShow::Index,
DiffStatusType::WorkdirToIndex => git2::StatusShow::Workdir,
};
let mut staged_changes = Vec::new();
let mut unstaged_changes = Vec::new();
let statuses = repo.statuses(Some(&mut status_options(false, show_opt)))
.map_err_with_prefix("Failed to get statuses:")?;

for entry in statuses.iter() {
let status = entry.status();
let relative_path = PathBuf::from(String::from_utf8_lossy(entry.path_bytes()).to_string());
Expand All @@ -89,113 +89,48 @@ pub fn get_diff_statuses(diff_status_type: DiffStatusType, repo: &Repository, in
continue;
}

let should_not_be_present = match diff_status_type {
DiffStatusType::IndexToHead => is_changed_in_wt(status) || status.is_index_renamed(),
DiffStatusType::WorkdirToIndex => is_changed_in_index(status) || status.is_wt_renamed(),
let should_not_be_present = match show_opt {
git2::StatusShow::Index => is_changed_in_wt(status) || status.is_index_renamed(),
git2::StatusShow::Workdir => is_changed_in_index(status) || status.is_wt_renamed(),
git2::StatusShow::IndexAndWorkdir => status.is_index_renamed() || status.is_wt_renamed(),
};
if should_not_be_present {
tracing::error!("File status is {:?} for file {:?}, which should not be present due to status options.", status, relative_path);
continue;
}

let file_change_status = match diff_status_type {
DiffStatusType::IndexToHead => {
if is_changed_in_index(status) {
match status {
s if s.is_index_new() => Some(FileChangeStatus::ADDED),
s if s.is_index_deleted() => Some(FileChangeStatus::DELETED),
_ => Some(FileChangeStatus::MODIFIED),
}
} else {
None
}
},
DiffStatusType::WorkdirToIndex => {
if is_changed_in_wt(status) {
match status {
s if s.is_wt_new() => Some(FileChangeStatus::ADDED),
s if s.is_wt_deleted() => Some(FileChangeStatus::DELETED),
_ => Some(FileChangeStatus::MODIFIED),
}
} else {
None
}
},
let absolute_path = if include_abs_paths && (is_changed_in_index(status) || is_changed_in_wt(status)) {
canonical_path(repo_workdir.join(&relative_path).to_string_lossy())
} else {
PathBuf::new()
};

if let Some(status) = file_change_status {
result.push(FileChange {
status,
absolute_path: if include_abs_paths {
canonical_path(repo_workdir.join(&relative_path).to_string_lossy().to_string())
} else {
PathBuf::new()
if is_changed_in_index(status) {
staged_changes.push(FileChange {
status: match status {
s if s.is_index_new() => FileChangeStatus::ADDED,
s if s.is_index_deleted() => FileChangeStatus::DELETED,
_ => FileChangeStatus::MODIFIED,
},
relative_path,
absolute_path: absolute_path.clone(),
relative_path: relative_path.clone(),
});
}
}

Ok(result)
}

pub fn get_diff_statuses_workdir_to_head(repository: &Repository) -> Result<Vec<FileChange>, String> {
let repository_workdir = repository.workdir()
.ok_or("Failed to get workdir from repository".to_string())?;

let head = repository.head().map_err_with_prefix("Failed to get HEAD:")?;
let tree = head.peel_to_tree().map_err_with_prefix("Failed to get HEAD tree:")?;

let mut diff_opts = git2::DiffOptions::new();
diff_opts
.include_untracked(true)
.recurse_untracked_dirs(true)
.show_untracked_content(true)
.include_ignored(false)
.include_unmodified(false)
.update_index(true)
.include_unreadable(false)
.recurse_ignored_dirs(false)
.disable_pathspec_match(true)
.include_typechange(false)
.show_binary(false);

let diff = repository.diff_tree_to_workdir(Some(&tree), Some(&mut diff_opts))
.map_err_with_prefix("Failed to get diff:")?;

let mut result = Vec::new();
diff.print(git2::DiffFormat::NameStatus, |_delta, _hunk, line| {
// Format is "X\tpath" where X is status code
let line_content = String::from_utf8_lossy(line.content()).to_string();
if let Some((status_str, path)) = line_content.split_once('\t') {
let status = match status_str {
"A" | "?" => Some(FileChangeStatus::ADDED),
"D" => Some(FileChangeStatus::DELETED),
"M" | "T" | "U" => Some(FileChangeStatus::MODIFIED),
"R" | "C" | " " | "!" | "X" => {
tracing::error!("Status {status_str} found for {path}, which should not be present due to status options.");
None
if is_changed_in_wt(status) {
unstaged_changes.push(FileChange {
status: match status {
s if s.is_wt_new() => FileChangeStatus::ADDED,
s if s.is_wt_deleted() => FileChangeStatus::DELETED,
_ => FileChangeStatus::MODIFIED,
},
_ => {
tracing::error!("Unknown status {status_str} found for {path}.");
None
}
};

if let Some(status) = status {
let relative_path = PathBuf::from(path.trim());
let absolute_path = canonical_path(repository_workdir.join(&relative_path).to_string_lossy().to_string());
result.push(FileChange {
status,
relative_path,
absolute_path,
});
}
absolute_path,
relative_path,
});
}
true
}).map_err_with_prefix("Failed to process diff:")?;
}

Ok(result)
Ok((staged_changes, unstaged_changes))
}

pub fn get_diff_statuses_index_to_commit(repository: &Repository, commit_oid: &git2::Oid, include_abs_paths: bool) -> Result<Vec<FileChange>, String> {
Expand All @@ -205,7 +140,7 @@ pub fn get_diff_statuses_index_to_commit(repository: &Repository, commit_oid: &g

repository.set_head_detached(commit_oid.clone()).map_err_with_prefix("Failed to set HEAD:")?;

let result = get_diff_statuses(DiffStatusType::IndexToHead, repository, include_abs_paths);
let result = get_diff_statuses(git2::StatusShow::Index, repository, include_abs_paths);

let restore_result = match (&original_head_ref, original_head_oid) {
(Some(head_ref), _) => repository.set_head(head_ref),
Expand All @@ -218,7 +153,7 @@ pub fn get_diff_statuses_index_to_commit(repository: &Repository, commit_oid: &g
return Err(format!("{}\nFailed to restore head: {}", prev_err, restore_err));
}

result
result.map(|(staged_changes, _unstaged_changes)| staged_changes)
}

pub fn stage_changes(repository: &Repository, file_changes: &Vec<FileChange>) -> Result<(), String> {
Expand Down Expand Up @@ -293,47 +228,22 @@ pub fn get_commit_datetime(repository: &Repository, commit_oid: &Oid) -> Result<
.ok_or_else(|| "Failed to get commit datetime".to_string())
}

pub fn git_diff<'repo>(repository: &'repo Repository, file_changes: &Vec<FileChange>) -> Result<git2::Diff<'repo>, String> {
pub fn git_diff_head_to_workdir<'repo>(repository: &'repo Repository) -> Result<git2::Diff<'repo>, String> {
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.relative_path);
}

let mut sorted_file_changes = file_changes.clone();
sorted_file_changes.sort_by_key(|fc| {
std::fs::metadata(&fc.relative_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 &sorted_file_changes {
match file_change.status {
FileChangeStatus::ADDED | FileChangeStatus::MODIFIED => {
index.add_path(&file_change.relative_path)
.map_err(|e| format!("Failed to add file to index: {}", e))?;
},
FileChangeStatus::DELETED => {
index.remove_path(&file_change.relative_path)
.map_err(|e| format!("Failed to remove file from 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))?;

let head = repository.head().and_then(|head_ref| head_ref.peel_to_tree())
.map_err(|e| format!("Failed to get HEAD tree: {}", e))?;

let diff = repository.diff_tree_to_tree(Some(&head), Some(&new_tree), Some(&mut diff_options))
let diff = repository.diff_tree_to_workdir(Some(&head), Some(&mut diff_options))
.map_err(|e| format!("Failed to generate diff: {}", e))?;

Ok(diff)
}

pub fn git_diff_as_string(repository: &Repository, file_changes: &Vec<FileChange>, max_size: usize) -> Result<String, String> {
let diff = git_diff(repository, file_changes)?;
pub fn git_diff_head_to_workdir_as_string(repository: &Repository, max_size: usize) -> Result<String, String> {
let diff = git_diff_head_to_workdir(repository)?;

let mut diff_str = String::new();
diff.print(git2::DiffFormat::Patch, |_, _, line| {
Expand Down
2 changes: 1 addition & 1 deletion refact-agent/engine/src/http/routers/v1/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ pub async fn handle_v1_git_commit(
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) {
if let Err(stage_err) = stage_changes(&repository, &commit.unstaged_changes) {
error_log.push(git_error(stage_err));
continue;
}
Expand Down
Loading