Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Commit link #472

Merged
merged 8 commits into from
Dec 11, 2024
210 changes: 165 additions & 45 deletions src/git.rs
Original file line number Diff line number Diff line change
@@ -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<FileChange>,
}

#[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<Vec<PathBuf>> {
let repository = Repository::open(repository_path)
Expand Down Expand Up @@ -28,61 +64,91 @@ pub fn git_ls_files(repository_path: &PathBuf) -> Option<Vec<PathBuf>> {
}

/// Similar to git checkout -b <branch_name>
pub fn create_or_checkout_to_branch<'repo>(repository: &'repo Repository, branch_name: &str) -> Result<Branch<'repo>, 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<Branch<'repo>, 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<FileChange>) -> 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<Vec<FileChange>, 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<Oid, String> {
Expand All @@ -104,24 +170,34 @@ 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(
Some(branch_ref_name), &signature, &signature, message, &tree, &[&parent_commit]
).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<String, String> {
/// Similar to `git diff`, from specified file changes.
pub fn git_diff(repository: &Repository, file_changes: &Vec<FileChange>, max_size: usize) -> Result<String, 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.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))?;

Expand All @@ -133,10 +209,54 @@ pub fn git_diff_from_all_changes(repository: &Repository) -> Result<String, Stri

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<ARwLock<GlobalContext>>) -> Vec<CommitInfo> {
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
}
4 changes: 2 additions & 2 deletions src/http/routers/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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

Expand Down
Loading