use git2::{Repository, Signature, StatusOptions};
use std::collections::HashSet;
#[allow(unused_imports)]
use std::path::Path;
use std::path::PathBuf;
use thiserror::Error as ThisError;
use tracing::debug;
#[derive(Debug, ThisError)]
pub enum Error {
#[error("Failed to open repository: {message}")]
GitRepository {
message: String,
#[source]
source: git2::Error,
},
#[error("No work directory found for the repository")]
NoWorkdir,
#[error("Failed to get HEAD commit: {0}")]
HeadCommit(#[source] git2::Error),
#[error("Failed to get tree from commit: {0}")]
TreeFromCommit(#[source] git2::Error),
#[error("Failed to load repository index: {0}")]
RepositoryIndex(#[source] git2::Error),
#[error("Failed to get repository status: {0}")]
RepositoryStatus(#[source] git2::Error),
#[error("Failed to restore index: {0}")]
RestoreIndex(#[source] git2::Error),
#[error("Failed to canonicalize path '{path}': {source}")]
CanonicalizePath {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("Path '{file_path}' is outside the work directory '{workdir}'")]
PathOutsideWorkdir {
file_path: PathBuf,
workdir: PathBuf,
},
#[error("Failed to add file '{file_path}' to index: {source}")]
AddFileToIndex {
file_path: PathBuf,
#[source]
source: git2::Error,
},
#[error("Failed to write index: {0}")]
WriteIndex(#[source] git2::Error),
#[error("Failed to write tree: {0}")]
WriteTree(#[source] git2::Error),
#[error("Failed to find tree '{oid}': {source}")]
FindTree {
oid: git2::Oid,
#[source]
source: git2::Error,
},
#[error("Failed to create signature: {0}")]
CreateSignature(#[source] git2::Error),
#[error("Failed to create commit: {0}")]
CreateCommit(#[source] git2::Error),
#[error("Failed to find commit '{oid}': {source}")]
FindCommit {
oid: git2::Oid,
#[source]
source: git2::Error,
},
#[error("Git error: {0}")]
Git2(#[from] git2::Error),
}
pub fn git_commit_files(
root_path: &Path,
files_to_commit: &[PathBuf],
message: &str,
) -> Result<(), Error> {
debug!(
"Running git_commit_files wth message {} on files {:?}",
message, files_to_commit
);
let repo = Repository::discover(root_path).map_err(|e| Error::GitRepository {
message: "Failed to open repository".to_string(),
source: e,
})?;
let workdir = repo.workdir().ok_or(Error::NoWorkdir)?;
let files_to_commit_set: HashSet<PathBuf> = files_to_commit.iter().cloned().collect();
let head_commit = repo
.head()
.and_then(|head| head.peel_to_commit())
.map_err(Error::HeadCommit)?;
let head_tree = head_commit.tree().map_err(Error::TreeFromCommit)?;
let mut index = repo.index().map_err(Error::RepositoryIndex)?;
let mut files_to_re_stage: Vec<PathBuf> = Vec::new();
let mut status_options = StatusOptions::new();
status_options
.include_ignored(false)
.include_untracked(false)
.recurse_untracked_dirs(false)
.exclude_submodules(true);
let statuses = repo
.statuses(Some(&mut status_options))
.map_err(Error::RepositoryStatus)?;
for entry in statuses.iter() {
if let Some(path_str) = entry.path() {
let path = PathBuf::from(path_str);
let status = entry.status();
if (status.is_index_new()
|| status.is_index_modified()
|| status.is_index_deleted()
|| status.is_index_renamed()
|| status.is_index_typechange())
&& !files_to_commit_set.contains(&path)
{
files_to_re_stage.push(path);
}
}
}
index.read_tree(&head_tree).map_err(Error::RestoreIndex)?;
for path in files_to_commit {
let canonical_path = path.canonicalize().map_err(|e| Error::CanonicalizePath {
path: path.clone(),
source: e,
})?;
let canonical_workdir = workdir
.canonicalize()
.map_err(|e| Error::CanonicalizePath {
path: workdir.to_path_buf(),
source: e,
})?;
let relative_path = canonical_path
.strip_prefix(&canonical_workdir)
.map_err(|_| Error::PathOutsideWorkdir {
file_path: path.clone(),
workdir: workdir.to_path_buf(),
})?;
index
.add_path(relative_path)
.map_err(|e| Error::AddFileToIndex {
file_path: path.clone(),
source: e,
})?;
}
index.write().map_err(Error::WriteIndex)?;
let tree_oid = index.write_tree().map_err(Error::WriteTree)?;
let tree = repo.find_tree(tree_oid).map_err(|e| Error::FindTree {
oid: tree_oid,
source: e,
})?;
let signature = Signature::now("vespe", "vespe@example.com") .map_err(Error::CreateSignature)?;
let new_commit_oid = repo
.commit(
Some("HEAD"), &signature,
&signature,
message,
&tree,
&[&head_commit], )
.map_err(Error::CreateCommit)?;
let new_head_commit = repo
.find_commit(new_commit_oid)
.map_err(|e| Error::FindCommit {
oid: new_commit_oid,
source: e,
})?;
let new_head_tree = new_head_commit.tree().map_err(Error::TreeFromCommit)?;
index
.read_tree(&new_head_tree)
.map_err(Error::RestoreIndex)?;
for path in files_to_re_stage {
let canonical_path = path.canonicalize().map_err(|e| Error::CanonicalizePath {
path: path.clone(),
source: e,
})?;
let canonical_workdir = workdir
.canonicalize()
.map_err(|e| Error::CanonicalizePath {
path: workdir.to_path_buf(),
source: e,
})?;
let relative_path = canonical_path
.strip_prefix(&canonical_workdir)
.map_err(|_| Error::PathOutsideWorkdir {
file_path: path.clone(),
workdir: workdir.to_path_buf(),
})?;
index
.add_path(relative_path)
.map_err(|e| Error::AddFileToIndex {
file_path: path.clone(),
source: e,
})?;
}
index.write().map_err(Error::WriteIndex)?;
debug!("Commit created with id {}", new_commit_oid);
Ok(())
}
pub fn is_in_git_repository(root_path: &Path) -> Result<bool, Error> {
match Repository::discover(&root_path) {
Ok(_) => Ok(true), Err(e) => {
if e.code() == git2::ErrorCode::NotFound {
Ok(false)
} else {
Err(e.into()) }
}
}
}