summaryrefslogtreecommitdiff
path: root/src/utils.rs
blob: dc0c082f7d9a67768d4d08b5fc7a964dcfa265ba (plain)
use std::path::Path;

use git2::{Commit, MergeOptions, Oid, Repository, Tree};
use is_executable::is_executable;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum TreeError {
	#[error(transparent)]
	Io(#[from] std::io::Error),
	#[error(transparent)]
	Git(#[from] git2::Error),
}

pub fn current_branch_ref(repository: &Repository) -> Result<String, git2::Error> {
	let head = repository.head()?;
	let ref_name = match head.kind() {
		Some(git2::ReferenceType::Symbolic) => head.symbolic_target(),
		_ => head.name(),
	}
	.ok_or(git2::Error::new(
		git2::ErrorCode::Invalid,
		git2::ErrorClass::Reference,
		"The current branch's name is not valid UTF-8",
	))?
	.to_string();
	Ok(ref_name)
}

pub fn current_branch(repository: &Repository) -> Result<String, git2::Error> {
	let ref_name = current_branch_ref(repository)?;
	let branch_name = ref_name
		.strip_prefix("refs/heads/")
		.unwrap_or(&ref_name)
		.to_string();
	Ok(branch_name)
}

pub fn filemode_for_dir_entry(path: &Path) -> std::io::Result<i32> {
	let metadata = path.metadata()?;
	Ok(if metadata.is_dir() {
		0o040000
	} else if metadata.is_symlink() {
		0o120000
	} else if is_executable(path) {
		0o100755
	} else {
		0o100644
	})
}

pub fn path_to_tree(repository: &Repository, path: &Path) -> Result<Oid, TreeError> {
	let workdir = repository.workdir().expect("a non-bare repo");
	if path.is_dir() {
		let mut tree = repository.treebuilder(None)?;
		for entry in path.read_dir()? {
			let entry = entry?;
			let full_path = entry.path();
			let relative_path = full_path.strip_prefix(workdir).unwrap_or(path);
			if repository.is_path_ignored(relative_path)? {
				continue;
			}

			let filemode = filemode_for_dir_entry(&entry.path())?;
			let oid = path_to_tree(repository, &entry.path())?;
			let filename = entry.file_name();
			tree.insert(filename, oid, filemode)?;
		}
		Ok(tree.write()?)
	} else {
		Ok(repository.blob_path(path)?)
	}
}

pub fn workdir_to_tree(repository: &Repository) -> Result<Oid, TreeError> {
	let Some(workdir) = repository.workdir() else {
		return Err(TreeError::Git(git2::Error::new(
			git2::ErrorCode::BareRepo,
			git2::ErrorClass::Repository,
			"git-autosave does not support bare repositories",
		)));
	};

	path_to_tree(repository, workdir)
}

pub fn merge_commit_with_tree(
	repository: &Repository,
	commit: &Commit<'_>,
	workdir: &Tree<'_>,
) -> Result<Oid, TreeError> {
	Ok(repository
		.merge_trees(
			&repository
				.find_commit(
					repository
						.merge_base(repository.head()?.peel_to_commit()?.id(), commit.id())?,
				)?
				.tree()?,
			workdir,
			&commit.tree()?,
			Some(MergeOptions::new().find_renames(true).patience(true)),
		)?
		.write_tree_to(repository)?)
}