use std::path::Path;
use git2::{FileMode, Oid, Repository, Signature};
use crate::error::Result;
pub fn commit_to_branch(
repo_path: &Path,
branch_name: &str,
file_path: &Path,
content: &[u8],
message: &str,
) -> Result<Option<Oid>> {
let repo = Repository::discover(repo_path)?;
let signature = repo
.signature()
.or_else(|_| Signature::now("itack", "itack@localhost"))?;
let file_path_str = file_path.to_string_lossy();
let blob_oid = repo.blob(content)?;
let branch_ref = format!("refs/heads/{}", branch_name);
let parent_commit = match repo.find_reference(&branch_ref) {
Ok(reference) => Some(reference.peel_to_commit()?),
Err(e) if e.code() == git2::ErrorCode::NotFound => None,
Err(e) => return Err(e.into()),
};
let new_tree = if let Some(ref parent) = parent_commit {
let parent_tree = parent.tree()?;
let mut builder = repo.treebuilder(Some(&parent_tree))?;
build_nested_tree(&repo, &mut builder, &file_path_str, blob_oid)?;
let tree_oid = builder.write()?;
repo.find_tree(tree_oid)?
} else {
let mut builder = repo.treebuilder(None)?;
build_nested_tree(&repo, &mut builder, &file_path_str, blob_oid)?;
let tree_oid = builder.write()?;
repo.find_tree(tree_oid)?
};
if let Some(ref parent) = parent_commit
&& parent.tree()?.id() == new_tree.id()
{
return Ok(None);
}
let parents: Vec<&git2::Commit> = parent_commit.iter().collect();
let commit_oid = repo.commit(
Some(&branch_ref),
&signature,
&signature,
message,
&new_tree,
&parents,
)?;
Ok(Some(commit_oid))
}
fn build_nested_tree(
repo: &Repository,
builder: &mut git2::TreeBuilder,
path: &str,
blob_oid: Oid,
) -> Result<()> {
let parts: Vec<&str> = path.split('/').collect();
if parts.len() == 1 {
builder.insert(parts[0], blob_oid, FileMode::Blob.into())?;
} else {
let dir_name = parts[0];
let remaining_path = parts[1..].join("/");
let existing_tree = builder
.get(dir_name)?
.and_then(|entry| repo.find_tree(entry.id()).ok());
let mut sub_builder = repo.treebuilder(existing_tree.as_ref())?;
build_nested_tree(repo, &mut sub_builder, &remaining_path, blob_oid)?;
let sub_tree_oid = sub_builder.write()?;
builder.insert(dir_name, sub_tree_oid, FileMode::Tree.into())?;
}
Ok(())
}
pub fn read_file_from_branch(
repo_path: &Path,
branch_name: &str,
file_path: &Path,
) -> Result<Option<Vec<u8>>> {
let repo = Repository::discover(repo_path)?;
let branch_ref = format!("refs/heads/{}", branch_name);
let reference = match repo.find_reference(&branch_ref) {
Ok(r) => r,
Err(e) if e.code() == git2::ErrorCode::NotFound => return Ok(None),
Err(e) => return Err(e.into()),
};
let commit = reference.peel_to_commit()?;
let tree = commit.tree()?;
let file_path_str = file_path.to_string_lossy();
match tree.get_path(std::path::Path::new(file_path_str.as_ref())) {
Ok(entry) => {
let blob = repo.find_blob(entry.id())?;
Ok(Some(blob.content().to_vec()))
}
Err(e) if e.code() == git2::ErrorCode::NotFound => Ok(None),
Err(e) => Err(e.into()),
}
}
pub fn remove_file_from_branch(
repo_path: &Path,
branch_name: &str,
file_path: &Path,
message: &str,
) -> Result<Option<Oid>> {
let repo = Repository::discover(repo_path)?;
let signature = repo
.signature()
.or_else(|_| Signature::now("itack", "itack@localhost"))?;
let file_path_str = file_path.to_string_lossy();
let branch_ref = format!("refs/heads/{}", branch_name);
let reference = repo.find_reference(&branch_ref)?;
let parent_commit = reference.peel_to_commit()?;
let parent_tree = parent_commit.tree()?;
let parts: Vec<&str> = file_path_str.split('/').collect();
let new_tree = if parts.len() == 1 {
let mut builder = repo.treebuilder(Some(&parent_tree))?;
builder.remove(parts[0])?;
let tree_oid = builder.write()?;
repo.find_tree(tree_oid)?
} else {
let dir_name = parts[0];
let file_name = parts[1..].join("/");
let dir_entry = match parent_tree.get_name(dir_name) {
Some(entry) => entry,
None => return Ok(None),
};
let dir_tree = repo.find_tree(dir_entry.id())?;
let mut sub_builder = repo.treebuilder(Some(&dir_tree))?;
if sub_builder.get(&file_name)?.is_none() {
return Ok(None);
}
sub_builder.remove(&file_name)?;
let sub_tree_oid = sub_builder.write()?;
let mut builder = repo.treebuilder(Some(&parent_tree))?;
builder.insert(dir_name, sub_tree_oid, FileMode::Tree.into())?;
let tree_oid = builder.write()?;
repo.find_tree(tree_oid)?
};
if parent_tree.id() == new_tree.id() {
return Ok(None);
}
let commit_oid = repo.commit(
Some(&branch_ref),
&signature,
&signature,
message,
&new_tree,
&[&parent_commit],
)?;
Ok(Some(commit_oid))
}
pub fn find_issue_in_branch(
repo_path: &Path,
branch_name: &str,
issue_id: u32,
) -> Result<Option<std::path::PathBuf>> {
let repo = Repository::discover(repo_path)?;
let branch_ref = format!("refs/heads/{}", branch_name);
let reference = match repo.find_reference(&branch_ref) {
Ok(r) => r,
Err(e) if e.code() == git2::ErrorCode::NotFound => return Ok(None),
Err(e) => return Err(e.into()),
};
let commit = reference.peel_to_commit()?;
let tree = commit.tree()?;
let itack_entry = match tree.get_name(".itack") {
Some(entry) => entry,
None => return Ok(None),
};
let itack_tree = repo.find_tree(itack_entry.id())?;
let suffix = format!("-issue-{:03}.md", issue_id);
for entry in itack_tree.iter() {
if let Some(name) = entry.name()
&& name.ends_with(&suffix)
{
return Ok(Some(std::path::PathBuf::from(".itack").join(name)));
}
}
let old_name = format!("{}.md", issue_id);
if itack_tree.get_name(&old_name).is_some() {
return Ok(Some(std::path::PathBuf::from(".itack").join(old_name)));
}
Ok(None)
}